@nocobase/plugin-workflow-mailer 2.2.20 → 2.2.21

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 (173) hide show
  1. package/dist/externalVersion.js +6 -6
  2. package/dist/node_modules/joi/package.json +1 -1
  3. package/dist/node_modules/nodemailer/dist/cjs/addressparser/index.d.ts +59 -0
  4. package/dist/node_modules/nodemailer/dist/cjs/addressparser/index.js +790 -0
  5. package/dist/node_modules/nodemailer/dist/cjs/base64/index.d.ts +36 -0
  6. package/dist/node_modules/nodemailer/dist/cjs/base64/index.js +119 -0
  7. package/dist/node_modules/nodemailer/dist/cjs/dkim/index.d.ts +52 -0
  8. package/dist/node_modules/nodemailer/dist/cjs/dkim/index.js +228 -0
  9. package/dist/node_modules/nodemailer/dist/cjs/dkim/message-parser.d.ts +48 -0
  10. package/dist/node_modules/nodemailer/dist/cjs/dkim/message-parser.js +180 -0
  11. package/dist/node_modules/nodemailer/dist/cjs/dkim/relaxed-body.d.ts +36 -0
  12. package/dist/node_modules/nodemailer/dist/cjs/dkim/relaxed-body.js +148 -0
  13. package/dist/node_modules/nodemailer/dist/cjs/dkim/sign.d.ts +52 -0
  14. package/dist/node_modules/nodemailer/dist/cjs/dkim/sign.js +165 -0
  15. package/dist/node_modules/nodemailer/dist/cjs/errors.d.ts +96 -0
  16. package/dist/node_modules/nodemailer/dist/cjs/errors.js +70 -0
  17. package/dist/node_modules/nodemailer/dist/cjs/fetch/cookies.d.ts +107 -0
  18. package/dist/node_modules/nodemailer/dist/cjs/fetch/cookies.js +282 -0
  19. package/dist/node_modules/nodemailer/dist/cjs/fetch/index.d.ts +65 -0
  20. package/dist/node_modules/nodemailer/dist/cjs/fetch/index.js +384 -0
  21. package/dist/node_modules/nodemailer/dist/cjs/json-transport/index.d.ts +64 -0
  22. package/dist/node_modules/nodemailer/dist/cjs/json-transport/index.js +99 -0
  23. package/dist/node_modules/nodemailer/dist/cjs/mail-composer/index.d.ts +174 -0
  24. package/dist/node_modules/nodemailer/dist/cjs/mail-composer/index.js +613 -0
  25. package/dist/node_modules/nodemailer/dist/cjs/mailer/index.d.ts +237 -0
  26. package/dist/node_modules/nodemailer/dist/cjs/mailer/index.js +415 -0
  27. package/dist/node_modules/nodemailer/dist/cjs/mailer/mail-message.d.ts +97 -0
  28. package/dist/node_modules/nodemailer/dist/cjs/mailer/mail-message.js +342 -0
  29. package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/index.d.ts +161 -0
  30. package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/index.js +787 -0
  31. package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/mime-types.d.ts +2 -0
  32. package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/mime-types.js +2103 -0
  33. package/dist/node_modules/nodemailer/dist/cjs/mime-node/index.d.ts +333 -0
  34. package/dist/node_modules/nodemailer/dist/cjs/mime-node/index.js +1493 -0
  35. package/dist/node_modules/nodemailer/dist/cjs/mime-node/last-newline.d.ts +5 -0
  36. package/dist/node_modules/nodemailer/dist/cjs/mime-node/last-newline.js +32 -0
  37. package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-unix.d.ts +9 -0
  38. package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-unix.js +40 -0
  39. package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-windows.d.ts +10 -0
  40. package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-windows.js +47 -0
  41. package/dist/node_modules/nodemailer/dist/cjs/nodemailer.d.ts +124 -0
  42. package/dist/node_modules/nodemailer/dist/cjs/nodemailer.js +1 -0
  43. package/dist/node_modules/nodemailer/dist/cjs/package-info.d.ts +3 -0
  44. package/dist/node_modules/nodemailer/dist/cjs/package-info.js +7 -0
  45. package/dist/node_modules/nodemailer/dist/cjs/package.json +3 -0
  46. package/dist/node_modules/nodemailer/dist/cjs/punycode/index.d.ts +73 -0
  47. package/dist/node_modules/nodemailer/{lib → dist/cjs}/punycode/index.js +62 -111
  48. package/dist/node_modules/nodemailer/dist/cjs/qp/index.d.ts +32 -0
  49. package/dist/node_modules/nodemailer/dist/cjs/qp/index.js +207 -0
  50. package/dist/node_modules/nodemailer/dist/cjs/sendmail-transport/index.d.ts +69 -0
  51. package/dist/node_modules/nodemailer/dist/cjs/sendmail-transport/index.js +210 -0
  52. package/dist/node_modules/nodemailer/dist/cjs/ses-transport/index.d.ts +112 -0
  53. package/dist/node_modules/nodemailer/dist/cjs/ses-transport/index.js +252 -0
  54. package/dist/node_modules/nodemailer/dist/cjs/shared/index.d.ts +260 -0
  55. package/dist/node_modules/nodemailer/dist/cjs/shared/index.js +680 -0
  56. package/dist/node_modules/nodemailer/dist/cjs/shared/objects.d.ts +23 -0
  57. package/dist/node_modules/nodemailer/dist/cjs/shared/objects.js +43 -0
  58. package/dist/node_modules/nodemailer/dist/cjs/shared/url.d.ts +21 -0
  59. package/dist/node_modules/nodemailer/dist/cjs/shared/url.js +255 -0
  60. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/data-stream.d.ts +15 -0
  61. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/data-stream.js +108 -0
  62. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.d.ts +47 -0
  63. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.js +189 -0
  64. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/index.d.ts +416 -0
  65. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/index.js +1837 -0
  66. package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/index.d.ts +133 -0
  67. package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/index.js +569 -0
  68. package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/pool-resource.d.ts +62 -0
  69. package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/pool-resource.js +257 -0
  70. package/dist/node_modules/nodemailer/dist/cjs/smtp-transport/index.d.ts +156 -0
  71. package/dist/node_modules/nodemailer/dist/cjs/smtp-transport/index.js +371 -0
  72. package/dist/node_modules/nodemailer/dist/cjs/stream-transport/index.d.ts +65 -0
  73. package/dist/node_modules/nodemailer/dist/cjs/stream-transport/index.js +146 -0
  74. package/dist/node_modules/nodemailer/dist/cjs/well-known/index.d.ts +35 -0
  75. package/dist/node_modules/nodemailer/dist/cjs/well-known/index.js +41 -0
  76. package/dist/node_modules/nodemailer/dist/cjs/well-known/services.d.ts +2 -0
  77. package/dist/node_modules/nodemailer/dist/cjs/well-known/services.js +674 -0
  78. package/dist/node_modules/nodemailer/dist/cjs/xoauth2/index.d.ts +201 -0
  79. package/dist/node_modules/nodemailer/dist/cjs/xoauth2/index.js +426 -0
  80. package/dist/node_modules/nodemailer/dist/esm/addressparser/index.d.ts +59 -0
  81. package/dist/node_modules/nodemailer/{lib → dist/esm}/addressparser/index.js +361 -131
  82. package/dist/node_modules/nodemailer/dist/esm/base64/index.d.ts +36 -0
  83. package/dist/node_modules/nodemailer/{lib → dist/esm}/base64/index.js +31 -56
  84. package/dist/node_modules/nodemailer/dist/esm/dkim/index.d.ts +52 -0
  85. package/dist/node_modules/nodemailer/{lib → dist/esm}/dkim/index.js +49 -78
  86. package/dist/node_modules/nodemailer/dist/esm/dkim/message-parser.d.ts +48 -0
  87. package/dist/node_modules/nodemailer/{lib → dist/esm}/dkim/message-parser.js +64 -42
  88. package/dist/node_modules/nodemailer/dist/esm/dkim/relaxed-body.d.ts +36 -0
  89. package/dist/node_modules/nodemailer/dist/esm/dkim/relaxed-body.js +141 -0
  90. package/dist/node_modules/nodemailer/dist/esm/dkim/sign.d.ts +52 -0
  91. package/dist/node_modules/nodemailer/{lib → dist/esm}/dkim/sign.js +50 -46
  92. package/dist/node_modules/nodemailer/dist/esm/errors.d.ts +96 -0
  93. package/dist/node_modules/nodemailer/{lib → dist/esm}/errors.js +26 -18
  94. package/dist/node_modules/nodemailer/dist/esm/fetch/cookies.d.ts +107 -0
  95. package/dist/node_modules/nodemailer/dist/esm/fetch/cookies.js +242 -0
  96. package/dist/node_modules/nodemailer/dist/esm/fetch/index.d.ts +65 -0
  97. package/dist/node_modules/nodemailer/{lib → dist/esm}/fetch/index.js +80 -147
  98. package/dist/node_modules/nodemailer/dist/esm/json-transport/index.d.ts +64 -0
  99. package/dist/node_modules/nodemailer/{lib → dist/esm}/json-transport/index.js +19 -39
  100. package/dist/node_modules/nodemailer/dist/esm/mail-composer/index.d.ts +174 -0
  101. package/dist/node_modules/nodemailer/{lib → dist/esm}/mail-composer/index.js +199 -265
  102. package/dist/node_modules/nodemailer/dist/esm/mailer/index.d.ts +237 -0
  103. package/dist/node_modules/nodemailer/{lib → dist/esm}/mailer/index.js +128 -234
  104. package/dist/node_modules/nodemailer/dist/esm/mailer/mail-message.d.ts +97 -0
  105. package/dist/node_modules/nodemailer/{lib → dist/esm}/mailer/mail-message.js +62 -114
  106. package/dist/node_modules/nodemailer/dist/esm/mime-funcs/index.d.ts +161 -0
  107. package/dist/node_modules/nodemailer/dist/esm/mime-funcs/index.js +738 -0
  108. package/dist/node_modules/nodemailer/dist/esm/mime-funcs/mime-types.d.ts +2 -0
  109. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-funcs/mime-types.js +29 -42
  110. package/dist/node_modules/nodemailer/dist/esm/mime-node/index.d.ts +333 -0
  111. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/index.js +259 -372
  112. package/dist/node_modules/nodemailer/dist/esm/mime-node/last-newline.d.ts +5 -0
  113. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/last-newline.js +4 -10
  114. package/dist/node_modules/nodemailer/dist/esm/mime-node/le-unix.d.ts +9 -0
  115. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/le-unix.js +6 -11
  116. package/dist/node_modules/nodemailer/dist/esm/mime-node/le-windows.d.ts +10 -0
  117. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/le-windows.js +6 -13
  118. package/dist/node_modules/nodemailer/dist/esm/nodemailer.d.ts +124 -0
  119. package/dist/node_modules/nodemailer/dist/esm/nodemailer.js +166 -0
  120. package/dist/node_modules/nodemailer/dist/esm/package-info.d.ts +3 -0
  121. package/dist/node_modules/nodemailer/dist/esm/package-info.js +4 -0
  122. package/dist/node_modules/nodemailer/dist/esm/package.json +3 -0
  123. package/dist/node_modules/nodemailer/dist/esm/punycode/index.d.ts +73 -0
  124. package/dist/node_modules/nodemailer/dist/esm/punycode/index.js +405 -0
  125. package/dist/node_modules/nodemailer/dist/esm/qp/index.d.ts +32 -0
  126. package/dist/node_modules/nodemailer/{lib → dist/esm}/qp/index.js +38 -67
  127. package/dist/node_modules/nodemailer/dist/esm/sendmail-transport/index.d.ts +69 -0
  128. package/dist/node_modules/nodemailer/{lib → dist/esm}/sendmail-transport/index.js +51 -101
  129. package/dist/node_modules/nodemailer/dist/esm/ses-transport/index.d.ts +112 -0
  130. package/dist/node_modules/nodemailer/dist/esm/ses-transport/index.js +212 -0
  131. package/dist/node_modules/nodemailer/dist/esm/shared/index.d.ts +260 -0
  132. package/dist/node_modules/nodemailer/{lib → dist/esm}/shared/index.js +156 -272
  133. package/dist/node_modules/nodemailer/dist/esm/shared/objects.d.ts +23 -0
  134. package/dist/node_modules/nodemailer/{lib → dist/esm}/shared/objects.js +13 -17
  135. package/dist/node_modules/nodemailer/dist/esm/shared/url.d.ts +21 -0
  136. package/dist/node_modules/nodemailer/dist/esm/shared/url.js +214 -0
  137. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/data-stream.d.ts +15 -0
  138. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/data-stream.js +103 -0
  139. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.d.ts +47 -0
  140. package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-connection/http-proxy-client.js +49 -74
  141. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/index.d.ts +416 -0
  142. package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-connection/index.js +574 -794
  143. package/dist/node_modules/nodemailer/dist/esm/smtp-pool/index.d.ts +133 -0
  144. package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-pool/index.js +172 -284
  145. package/dist/node_modules/nodemailer/dist/esm/smtp-pool/pool-resource.d.ts +62 -0
  146. package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-pool/pool-resource.js +57 -97
  147. package/dist/node_modules/nodemailer/dist/esm/smtp-transport/index.d.ts +156 -0
  148. package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-transport/index.js +81 -179
  149. package/dist/node_modules/nodemailer/dist/esm/stream-transport/index.d.ts +65 -0
  150. package/dist/node_modules/nodemailer/{lib → dist/esm}/stream-transport/index.js +36 -74
  151. package/dist/node_modules/nodemailer/dist/esm/well-known/index.d.ts +35 -0
  152. package/dist/node_modules/nodemailer/{lib → dist/esm}/well-known/index.js +5 -16
  153. package/dist/node_modules/nodemailer/dist/esm/well-known/services.d.ts +2 -0
  154. package/dist/node_modules/nodemailer/dist/esm/well-known/services.js +671 -0
  155. package/dist/node_modules/nodemailer/dist/esm/xoauth2/index.d.ts +201 -0
  156. package/dist/node_modules/nodemailer/{lib → dist/esm}/xoauth2/index.js +125 -187
  157. package/dist/node_modules/nodemailer/{lib → dist}/well-known/services.json +9 -2
  158. package/dist/node_modules/nodemailer/package.json +1 -1
  159. package/package.json +3 -3
  160. package/dist/node_modules/nodemailer/.gitattributes +0 -6
  161. package/dist/node_modules/nodemailer/.ncurc.js +0 -9
  162. package/dist/node_modules/nodemailer/.prettierignore +0 -8
  163. package/dist/node_modules/nodemailer/.prettierrc +0 -12
  164. package/dist/node_modules/nodemailer/.prettierrc.js +0 -10
  165. package/dist/node_modules/nodemailer/.release-please-config.json +0 -9
  166. package/dist/node_modules/nodemailer/eslint.config.js +0 -88
  167. package/dist/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
  168. package/dist/node_modules/nodemailer/lib/fetch/cookies.js +0 -276
  169. package/dist/node_modules/nodemailer/lib/mime-funcs/index.js +0 -671
  170. package/dist/node_modules/nodemailer/lib/nodemailer.js +0 -1
  171. package/dist/node_modules/nodemailer/lib/ses-transport/index.js +0 -266
  172. package/dist/node_modules/nodemailer/lib/shared/url.js +0 -151
  173. package/dist/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -105
@@ -1,22 +1,21 @@
1
- 'use strict';
2
-
3
- const packageInfo = require('../../package.json');
4
- const { EventEmitter } = require('events');
5
- const net = require('net');
6
- const tls = require('tls');
7
- const os = require('os');
8
- const crypto = require('crypto');
9
- const DataStream = require('./data-stream');
10
- const { PassThrough } = require('stream');
11
- const shared = require('../shared');
12
-
1
+ import * as packageData from '../package-info.js';
2
+ import { EventEmitter } from 'node:events';
3
+ import net from 'node:net';
4
+ import tls from 'node:tls';
5
+ import os from 'node:os';
6
+ import crypto from 'node:crypto';
7
+ import DataStream from './data-stream.js';
8
+ import { PassThrough } from 'node:stream';
9
+ import * as shared from '../shared/index.js';
13
10
  // default timeout values in ms
14
11
  const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection to be established
15
12
  const SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity before disconnecting the client
16
13
  const GREETING_TIMEOUT = 30 * 1000; // how much to wait after connection is established but SMTP greeting is not receieved
17
14
  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
19
-
15
+ const TEARDOWN_NOOP = () => { }; // reusable no-op handler for absorbing errors during socket teardown
16
+ // how many bytes a single server response may occupy while it is still being received.
17
+ // Generous compared to any real reply, it only stops a peer that never completes one
18
+ const MAX_RESPONSE_SIZE = 1024 * 1024;
20
19
  /**
21
20
  * Re-interpret a server response stored in fake 8-bit byte-container form
22
21
  * (the result of chunk.toString('binary') in _onData) as UTF-8.
@@ -38,7 +37,27 @@ function decodeServerResponse(str) {
38
37
  // decoder for invalid bytes, which means we should return the original bytes intact.
39
38
  return utf8.includes('\uFFFD') ? str : utf8;
40
39
  }
41
-
40
+ /**
41
+ * True when the last line of a queued reply is a continuation ("250-..."), which means
42
+ * the rest of the reply is still on its way.
43
+ *
44
+ * Called with the byte-container form the queue holds (see _onData): the check only looks
45
+ * at leading ASCII digits and '-' of the last line, and a UTF-8 continuation byte is never
46
+ * 0x0A, so line boundaries and the tested prefix are the same before and after decoding.
47
+ * The last line is read with lastIndexOf rather than split() because a queue entry may hold
48
+ * a whole multiline reply and only its final line matters.
49
+ */
50
+ function isPartialResponse(str) {
51
+ return isPartialLine(str.slice(str.lastIndexOf('\n') + 1));
52
+ }
53
+ /**
54
+ * True when a single reply line is a continuation ("250-..."). Used where the line is
55
+ * already known to be one line, which skips the scan isPartialResponse needs to find the
56
+ * last line of a whole reply.
57
+ */
58
+ function isPartialLine(line) {
59
+ return /^\d+-/.test(line);
60
+ }
42
61
  /**
43
62
  * Generates a SMTP connection object
44
63
  *
@@ -47,13 +66,14 @@ function decodeServerResponse(str) {
47
66
  * * **port** - is the port to connect to (defaults to 587 or 465)
48
67
  * * **host** - is the hostname or IP address to connect to (defaults to 'localhost')
49
68
  * * **secure** - use SSL
50
- * * **ignoreTLS** - ignore server support for STARTTLS
51
- * * **requireTLS** - forces the client to use STARTTLS
69
+ * * **ignoreTLS** - ignore server support for STARTTLS (has no effect when requireTLS is set)
70
+ * * **requireTLS** - forces the client to use STARTTLS, takes precedence over ignoreTLS and opportunisticTLS
52
71
  * * **name** - the name of the client server
53
72
  * * **localAddress** - outbound address to bind to (see: http://nodejs.org/api/net.html#net_net_connect_options_connectionlistener)
54
73
  * * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 30 seconds)
55
74
  * * **connectionTimeout** - how many milliseconds to wait for the connection to establish (defaults to 2 minutes)
56
75
  * * **socketTimeout** - Time of inactivity until the connection is closed (defaults to 10 minutes)
76
+ * * **maxResponseSize** - Largest single server response to accept in bytes (defaults to 1 MB)
57
77
  * * **dnsTimeout** - Time to wait in ms for the DNS requests to be resolved (defaults to 30 seconds)
58
78
  * * **lmtp** - if true, uses LMTP instead of SMTP protocol
59
79
  * * **logger** - bunyan compatible logger interface
@@ -64,39 +84,34 @@ function decodeServerResponse(str) {
64
84
  *
65
85
  * @constructor
66
86
  * @namespace SMTP Client module
67
- * @param {Object} [options] Option properties
87
+ * @param [options] Option properties
68
88
  */
69
89
  class SMTPConnection extends EventEmitter {
70
90
  constructor(options) {
71
91
  super(options);
72
-
73
92
  this.id = crypto.randomBytes(8).toString('base64').replace(/\W/g, '');
74
93
  this.stage = 'init';
75
-
76
94
  this.options = options || {};
77
-
95
+ if (this.options.requireTLS && (this.options.ignoreTLS || this.options.opportunisticTLS)) {
96
+ // requireTLS wins, a contradictory configuration must not quietly fall back to plaintext.
97
+ // Copied so the caller's (possibly shared) options object is left as it was
98
+ this.options = Object.assign({}, this.options, { ignoreTLS: false, opportunisticTLS: false });
99
+ }
78
100
  this.secureConnection = !!this.options.secure;
79
101
  this.alreadySecured = !!this.options.secured;
80
-
81
102
  this.port = Number(this.options.port) || (this.secureConnection ? 465 : 587);
82
103
  this.host = this.options.host || 'localhost';
83
-
84
104
  this.servername = this.options.servername ? this.options.servername : !net.isIP(this.host) ? this.host : false;
85
-
86
105
  this.allowInternalNetworkInterfaces = this.options.allowInternalNetworkInterfaces || false;
87
-
88
106
  if (typeof this.options.secure === 'undefined' && this.port === 465) {
89
107
  // if secure option is not set but port is 465, then default to secure
90
108
  this.secureConnection = true;
91
109
  }
92
-
93
110
  this.name = (this.options.name || this._getHostname()).toString().replace(/[\r\n]+/g, '');
94
-
95
111
  this.logger = shared.getLogger(this.options, {
96
112
  component: this.options.component || 'smtp-connection',
97
113
  sid: this.id
98
114
  });
99
-
100
115
  this.customAuth = new Map();
101
116
  for (const key of Object.keys(this.options.customAuth || {})) {
102
117
  const mapKey = (key || '').toString().trim().toUpperCase();
@@ -104,201 +119,90 @@ class SMTPConnection extends EventEmitter {
104
119
  this.customAuth.set(mapKey, this.options.customAuth[key]);
105
120
  }
106
121
  }
107
-
108
- /**
109
- * Expose version nr, just for the reference
110
- * @type {String}
111
- */
112
- this.version = packageInfo.version;
113
-
114
- /**
115
- * If true, then the user is authenticated
116
- * @type {Boolean}
117
- */
122
+ this.version = packageData.version;
118
123
  this.authenticated = false;
119
-
120
- /**
121
- * If set to true, this instance is no longer active
122
- * @private
123
- */
124
124
  this.destroyed = false;
125
-
126
- /**
127
- * Defines if the current connection is secure or not. If not,
128
- * STARTTLS can be used if available
129
- * @private
130
- */
131
125
  this.secure = !!this.secureConnection;
132
-
133
- /**
134
- * Store incomplete messages coming from the server
135
- * @private
136
- */
137
126
  this._remainder = '';
138
-
139
- /**
140
- * Unprocessed responses from the server
141
- * @type {Array}
142
- */
143
127
  this._responseQueue = [];
144
-
128
+ this._responsePartial = false;
145
129
  this.lastServerResponse = false;
146
-
147
- /**
148
- * The socket connecting to the server
149
- * @public
150
- */
151
130
  this._socket = false;
152
-
153
- /**
154
- * Lists supported auth mechanisms
155
- * @private
156
- */
157
131
  this._supportedAuth = [];
158
-
159
- /**
160
- * Set to true, if EHLO response includes "AUTH".
161
- * If false then authentication is not tried
162
- */
163
132
  this.allowsAuth = false;
164
-
165
- /**
166
- * Includes current envelope (from, to)
167
- * @private
168
- */
169
133
  this._envelope = false;
170
-
171
- /**
172
- * Lists supported extensions
173
- * @private
174
- */
175
134
  this._supportedExtensions = [];
176
-
177
- /**
178
- * Defines the maximum allowed size for a single message
179
- * @private
180
- */
181
135
  this._maxAllowedSize = 0;
182
-
183
- /**
184
- * Function queue to run if a data chunk comes from the server
185
- * @private
186
- */
187
136
  this._responseActions = [];
188
137
  this._recipientQueue = [];
189
-
190
- /**
191
- * Timeout variable for waiting the greeting
192
- * @private
193
- */
194
138
  this._greetingTimeout = false;
195
-
196
- /**
197
- * Timeout variable for waiting the connection to start
198
- * @private
199
- */
200
139
  this._connectionTimeout = false;
201
-
202
- /**
203
- * If the socket is deemed already closed
204
- * @private
205
- */
206
140
  this._destroyed = false;
207
-
208
- /**
209
- * If the socket is already being closed
210
- * @private
211
- */
212
141
  this._closing = false;
213
-
214
- /**
215
- * Message DATA stream currently piped to the socket, if any. Tracked so
216
- * close() can unpipe it before tearing the socket down.
217
- * @private
218
- */
219
142
  this._currentDataStream = false;
220
-
221
- /**
222
- * Callbacks for socket's listeners
223
- */
143
+ this._pendingSend = false;
144
+ this._connectCallback = false;
224
145
  this._onSocketData = chunk => this._onData(chunk);
225
146
  this._onSocketError = error => this._onError(error, 'ESOCKET', false, 'CONN');
226
147
  this._onSocketClose = () => this._onClose();
227
148
  this._onSocketEnd = () => this._onEnd();
228
149
  this._onSocketTimeout = () => this._onTimeout();
229
-
230
- /**
231
- * Connection-phase error handler (supports fallback to alternative addresses)
232
- */
233
150
  this._onConnectionSocketError = err => this._onConnectionError(err, 'ESOCKET');
234
-
235
- /**
236
- * Connection attempt counter for fallback race condition protection
237
- * @private
238
- */
239
151
  this._connectionAttemptId = 0;
240
152
  }
241
-
242
153
  /**
243
154
  * Creates a connection to a SMTP server and sets up connection
244
155
  * listener
245
156
  */
246
157
  connect(connectCallback) {
247
158
  if (typeof connectCallback === 'function') {
159
+ this._connectCallback = connectCallback;
248
160
  this.once('connect', () => {
249
- this.logger.debug(
250
- {
251
- tnx: 'smtp'
252
- },
253
- 'SMTP handshake finished'
254
- );
161
+ this._connectCallback = false;
162
+ this.logger.debug({
163
+ tnx: 'smtp'
164
+ }, 'SMTP handshake finished');
255
165
  connectCallback();
256
166
  });
257
-
258
167
  const isDestroyedMessage = this._isDestroyedMessage('connect');
259
168
  if (isDestroyedMessage) {
260
169
  return connectCallback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'CONN'));
261
170
  }
262
171
  }
263
-
264
172
  let opts = {
265
173
  port: this.port,
266
174
  host: this.host,
267
175
  allowInternalNetworkInterfaces: this.allowInternalNetworkInterfaces,
268
176
  timeout: this.options.dnsTimeout || DNS_TIMEOUT
269
177
  };
270
-
271
178
  if (this.options.localAddress) {
272
179
  opts.localAddress = this.options.localAddress;
273
180
  }
274
-
275
181
  if (this.options.connection) {
276
182
  // connection is already opened
277
183
  this._socket = this.options.connection;
278
184
  this._setupConnectionHandlers();
279
-
280
185
  if (this.secureConnection && !this.alreadySecured) {
281
- setImmediate(() =>
282
- this._upgradeConnection(err => {
283
- if (err) {
284
- this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'CONN');
285
- return;
286
- }
287
- this._onConnect();
288
- })
289
- );
290
- } else {
186
+ setImmediate(() => this._upgradeConnection(err => {
187
+ if (err) {
188
+ this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'CONN');
189
+ return;
190
+ }
191
+ this._onConnect();
192
+ }));
193
+ }
194
+ else {
291
195
  setImmediate(() => this._onConnect());
292
196
  }
293
197
  return;
294
- } else if (this.options.socket) {
198
+ }
199
+ else if (this.options.socket) {
295
200
  // socket object is set up but not yet connected
296
201
  this._socket = this.options.socket;
297
202
  return this._resolveAndConnect(opts, _resolved => {
298
203
  try {
299
204
  this._socket.connect(this.port, this.host, () => {
300
205
  this._socket.setKeepAlive(true);
301
-
302
206
  // a `secure` connection over a caller-provided socket must still
303
207
  // perform the TLS handshake, otherwise AUTH and the message body
304
208
  // would be sent in cleartext despite the caller requesting TLS
@@ -311,58 +215,51 @@ class SMTPConnection extends EventEmitter {
311
215
  this._onConnect();
312
216
  });
313
217
  }
314
-
315
218
  this._onConnect();
316
219
  });
317
220
  this._setupConnectionHandlers();
318
- } catch (E) {
319
- return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
221
+ }
222
+ catch (E) {
223
+ setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
224
+ return;
320
225
  }
321
226
  });
322
- } else {
227
+ }
228
+ else {
323
229
  if (this.secureConnection) {
324
230
  Object.assign(opts, this.options.tls || {});
325
-
326
231
  // ensure servername for SNI
327
232
  if (this.servername && !opts.servername) {
328
233
  opts.servername = this.servername;
329
234
  }
330
235
  }
331
-
332
236
  return this._resolveAndConnect(opts, resolved => {
333
237
  // Store fallback addresses for retry on connection failure
334
238
  this._fallbackAddresses = (resolved._addresses || []).filter(addr => addr !== opts.host);
335
239
  this._connectOpts = Object.assign({}, opts);
336
-
337
240
  this._connectToHost(opts, this.secureConnection);
338
241
  });
339
242
  }
340
243
  }
341
-
342
244
  /**
343
245
  * Resolves the hostname and applies resolved values to opts,
344
246
  * then calls the provided callback with the resolved data
345
247
  *
346
- * @param {Object} opts Connection options (modified in place)
347
- * @param {Function} callback Called with resolved data on success
248
+ * @param opts Connection options (modified in place)
249
+ * @param callback Called with resolved data on success
250
+ * @internal
348
251
  */
349
252
  _resolveAndConnect(opts, callback) {
350
253
  return shared.resolveHostname(opts, (err, resolved) => {
351
254
  if (err) {
352
255
  return setImmediate(() => this._onError(err, 'EDNS', false, 'CONN'));
353
256
  }
354
- this.logger.debug(
355
- {
356
- tnx: 'dns',
357
- source: opts.host,
358
- resolved: resolved.host,
359
- cached: !!resolved.cached
360
- },
361
- 'Resolved %s as %s [cache %s]',
362
- opts.host,
363
- resolved.host,
364
- resolved.cached ? 'hit' : 'miss'
365
- );
257
+ this.logger.debug({
258
+ tnx: 'dns',
259
+ source: opts.host,
260
+ resolved: resolved.host,
261
+ cached: !!resolved.cached
262
+ }, 'Resolved %s as %s [cache %s]', opts.host, resolved.host, resolved.cached ? 'hit' : 'miss');
366
263
  for (const key of Object.keys(resolved)) {
367
264
  if (key.charAt(0) !== '_' && resolved[key]) {
368
265
  opts[key] = resolved[key];
@@ -371,26 +268,26 @@ class SMTPConnection extends EventEmitter {
371
268
  callback(resolved);
372
269
  });
373
270
  }
374
-
375
271
  /**
376
272
  * Attempts to connect to the specified host address
377
273
  *
378
- * @param {Object} opts Connection options
379
- * @param {Boolean} secure Whether to use TLS
274
+ * @param opts Connection options
275
+ * @param secure Whether to use TLS
276
+ * @internal
380
277
  */
381
278
  _connectToHost(opts, secure) {
382
279
  // If the client was closed while DNS resolution was in flight, do not open
383
280
  // a socket here: close() ran with this._socket still unset and so had
384
281
  // nothing to tear down, and _onConnect's remedial close() is a no-op once
385
- // _closing is set — the freshly connected socket would leak.
282
+ // _closing is set, the freshly connected socket would leak.
386
283
  if (this._destroyed || this._closing) {
387
284
  return;
388
285
  }
389
-
390
286
  this._connectionAttemptId++;
391
287
  const currentAttemptId = this._connectionAttemptId;
392
-
393
- const connectFn = secure ? tls.connect : net.connect;
288
+ const connectFn = secure
289
+ ? tls.connect
290
+ : net.connect;
394
291
  try {
395
292
  this._socket = connectFn(opts, () => {
396
293
  // Ignore callback if this is a stale connection attempt
@@ -401,54 +298,45 @@ class SMTPConnection extends EventEmitter {
401
298
  this._onConnect();
402
299
  });
403
300
  this._setupConnectionHandlers();
404
- } catch (E) {
405
- return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
301
+ }
302
+ catch (E) {
303
+ setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
304
+ return;
406
305
  }
407
306
  }
408
-
409
307
  /**
410
308
  * Sets up connection timeout and error handlers
309
+ * @internal
411
310
  */
412
311
  _setupConnectionHandlers() {
413
312
  this._connectionTimeout = setTimeout(() => {
414
313
  this._onConnectionError('Connection timeout', 'ETIMEDOUT');
415
314
  }, this.options.connectionTimeout || CONNECTION_TIMEOUT);
416
-
417
315
  this._socket.on('error', this._onConnectionSocketError);
418
316
  }
419
-
420
317
  /**
421
318
  * Handles connection errors with fallback to alternative addresses
422
319
  *
423
- * @param {Error|String} err Error object or message
424
- * @param {String} code Error code
320
+ * @param err Error object or message
321
+ * @param code Error code
322
+ * @internal
425
323
  */
426
324
  _onConnectionError(err, code) {
427
325
  clearTimeout(this._connectionTimeout);
428
-
429
326
  // Check if we have fallback addresses to try
430
327
  const canFallback = this._fallbackAddresses && this._fallbackAddresses.length && this.stage === 'init' && !this._destroyed;
431
-
432
328
  if (!canFallback) {
433
329
  // No more fallback addresses, report the error
434
330
  this._onError(err, code, false, 'CONN');
435
331
  return;
436
332
  }
437
-
438
333
  const nextHost = this._fallbackAddresses.shift();
439
-
440
- this.logger.info(
441
- {
442
- tnx: 'network',
443
- failedHost: this._connectOpts.host,
444
- nextHost,
445
- error: err.message || err
446
- },
447
- 'Connection to %s failed, trying %s',
448
- this._connectOpts.host,
449
- nextHost
450
- );
451
-
334
+ this.logger.info({
335
+ tnx: 'network',
336
+ failedHost: this._connectOpts.host,
337
+ nextHost,
338
+ error: err.message || err
339
+ }, 'Connection to %s failed, trying %s', this._connectOpts.host, nextHost);
452
340
  // Clean up current socket
453
341
  if (this._socket) {
454
342
  try {
@@ -457,17 +345,16 @@ class SMTPConnection extends EventEmitter {
457
345
  // after destroy), mirroring the guard used in close()
458
346
  this._socket.on('error', TEARDOWN_NOOP);
459
347
  this._socket.destroy();
460
- } catch (_E) {
348
+ }
349
+ catch (_E) {
461
350
  // ignore
462
351
  }
463
352
  this._socket = null;
464
353
  }
465
-
466
354
  // Update host and retry
467
355
  this._connectOpts.host = nextHost;
468
356
  this._connectToHost(this._connectOpts, this.secureConnection);
469
357
  }
470
-
471
358
  /**
472
359
  * Sends QUIT
473
360
  */
@@ -475,7 +362,6 @@ class SMTPConnection extends EventEmitter {
475
362
  this._sendCommand('QUIT');
476
363
  this._responseActions.push(this.close);
477
364
  }
478
-
479
365
  /**
480
366
  * Closes the connection to the server
481
367
  */
@@ -483,36 +369,37 @@ class SMTPConnection extends EventEmitter {
483
369
  clearTimeout(this._connectionTimeout);
484
370
  clearTimeout(this._greetingTimeout);
485
371
  this._responseActions = [];
486
-
487
372
  // allow to run this function only once
488
373
  if (this._closing) {
489
374
  return;
490
375
  }
491
376
  this._closing = true;
492
-
493
377
  const closeMethod = this.stage === 'init' ? 'destroy' : 'end';
494
-
495
- this.logger.debug(
496
- {
497
- tnx: 'smtp'
498
- },
499
- 'Closing connection to the server using "%s"',
500
- closeMethod
501
- );
502
-
378
+ this.logger.debug({
379
+ tnx: 'smtp'
380
+ }, 'Closing connection to the server using "%s"', closeMethod);
503
381
  const socket = (this._socket && this._socket.socket) || this._socket;
504
-
505
382
  // Detach any in-flight DATA stream from the socket so the source stream
506
383
  // can be garbage-collected once the socket is gone.
507
384
  if (this._currentDataStream) {
508
385
  try {
509
386
  this._currentDataStream.unpipe(this._socket);
510
- } catch (_E) {
387
+ }
388
+ catch (_E) {
511
389
  // ignore
512
390
  }
513
391
  this._currentDataStream = false;
514
392
  }
515
-
393
+ // Detach from the message stream as well. The listener is swapped for a no-op rather than
394
+ // removed, a stream destroyed with an error later on would otherwise throw it as unhandled
395
+ if (this._pendingSend) {
396
+ const { stream, onStreamError } = this._pendingSend;
397
+ if (stream) {
398
+ stream.removeListener('error', onStreamError);
399
+ stream.on('error', TEARDOWN_NOOP);
400
+ }
401
+ this._pendingSend = false;
402
+ }
516
403
  if (socket && !socket.destroyed) {
517
404
  try {
518
405
  // Clear socket timeout to prevent timer leaks
@@ -528,14 +415,13 @@ class SMTPConnection extends EventEmitter {
528
415
  // sending cleartext after TLS shutdown triggers ERR_SSL_BAD_RECORD_TYPE)
529
416
  socket.on('error', TEARDOWN_NOOP);
530
417
  socket[closeMethod]();
531
- } catch (_E) {
418
+ }
419
+ catch (_E) {
532
420
  // just ignore
533
421
  }
534
422
  }
535
-
536
423
  this._destroy();
537
424
  }
538
-
539
425
  /**
540
426
  * Authenticate user
541
427
  */
@@ -544,116 +430,108 @@ class SMTPConnection extends EventEmitter {
544
430
  if (isDestroyedMessage) {
545
431
  return callback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API'));
546
432
  }
547
-
548
433
  this._auth = authData || {};
549
434
  // Select SASL authentication method
550
435
  this._authMethod = (this._auth.method || '').toString().trim().toUpperCase() || false;
551
-
436
+ // XOAUTH2 needs a token generator or a custom handler, without either the method
437
+ // can not be run even when it is the only one the server advertised
438
+ const canUseXOAuth2 = !!this._auth.oauth2 || this.customAuth.has('XOAUTH2');
552
439
  if (!this._authMethod && this._auth.oauth2 && !this._auth.credentials) {
553
440
  this._authMethod = 'XOAUTH2';
554
- } else if (!this._authMethod || (this._authMethod === 'XOAUTH2' && !this._auth.oauth2)) {
555
- // use first supported
556
- this._authMethod = (this._supportedAuth[0] || 'PLAIN').toUpperCase().trim();
557
441
  }
558
-
559
- if (this._authMethod !== 'XOAUTH2' && (!this._auth.credentials || !this._auth.credentials.user || !this._auth.credentials.pass)) {
442
+ else if (!this._authMethod || (this._authMethod === 'XOAUTH2' && !this._auth.oauth2)) {
443
+ // use the first supported method that can be run
444
+ const supported = this._supportedAuth.find(method => method !== 'XOAUTH2' || canUseXOAuth2);
445
+ this._authMethod = (supported || 'PLAIN').toUpperCase().trim();
446
+ }
447
+ // a token login needs no credentials, every other method and every custom handler
448
+ // gets them filled in from the user and pass values
449
+ if ((this._authMethod !== 'XOAUTH2' || this.customAuth.has('XOAUTH2')) &&
450
+ (!this._auth.credentials || !this._auth.credentials.user || !this._auth.credentials.pass)) {
560
451
  if ((this._auth.user && this._auth.pass) || this.customAuth.has(this._authMethod)) {
561
452
  this._auth.credentials = {
562
453
  user: this._auth.user,
563
454
  pass: this._auth.pass,
564
455
  options: this._auth.options
565
456
  };
566
- } else {
457
+ }
458
+ else {
567
459
  return callback(this._formatError('Missing credentials for "' + this._authMethod + '"', 'EAUTH', false, 'API'));
568
460
  }
569
461
  }
570
-
571
462
  if (this.customAuth.has(this._authMethod)) {
572
463
  const handler = this.customAuth.get(this._authMethod);
573
464
  let lastResponse;
574
465
  let returned = false;
575
-
576
466
  const resolve = () => {
577
467
  if (returned) {
578
468
  return;
579
469
  }
580
470
  returned = true;
581
- this.logger.info(
582
- {
583
- tnx: 'smtp',
584
- username: this._auth.user,
585
- action: 'authenticated',
586
- method: this._authMethod
587
- },
588
- 'User %s authenticated',
589
- JSON.stringify(this._auth.user)
590
- );
471
+ this.logger.info({
472
+ tnx: 'smtp',
473
+ username: this._auth.user,
474
+ action: 'authenticated',
475
+ method: this._authMethod
476
+ }, 'User %s authenticated', JSON.stringify(this._auth.user));
591
477
  this.authenticated = true;
592
478
  callback(null, true);
593
479
  };
594
-
595
- const reject = err => {
480
+ const reject = (err) => {
596
481
  if (returned) {
597
482
  return;
598
483
  }
599
484
  returned = true;
600
485
  callback(this._formatError(err, 'EAUTH', lastResponse, 'AUTH ' + this._authMethod));
601
486
  };
602
-
487
+ // one implementation serves both sendCommand overloads, the promise is returned
488
+ // exactly when no callback was given
489
+ const sendCommand = (cmd, done) => {
490
+ let promise;
491
+ if (!done) {
492
+ promise = new Promise((resolve, reject) => {
493
+ done = shared.callbackPromise(resolve, reject);
494
+ });
495
+ }
496
+ this._responseActions.push(str => {
497
+ lastResponse = str;
498
+ let codes = str.match(/^(\d+)(?:\s(\d+\.\d+\.\d+))?\s/);
499
+ let data = {
500
+ command: cmd,
501
+ response: str
502
+ };
503
+ if (codes) {
504
+ data.status = Number(codes[1]) || 0;
505
+ if (codes[2]) {
506
+ data.code = codes[2];
507
+ }
508
+ data.text = str.substr(codes[0].length);
509
+ }
510
+ else {
511
+ data.text = str;
512
+ data.status = 0; // just in case we need to perform numeric comparisons
513
+ }
514
+ done(null, data);
515
+ });
516
+ setImmediate(() => this._sendCommand(cmd));
517
+ return promise;
518
+ };
603
519
  const handlerResponse = handler({
604
520
  auth: this._auth,
605
521
  method: this._authMethod,
606
-
607
522
  extensions: [].concat(this._supportedExtensions),
608
523
  authMethods: [].concat(this._supportedAuth),
609
524
  maxAllowedSize: this._maxAllowedSize || false,
610
-
611
- sendCommand: (cmd, done) => {
612
- let promise;
613
-
614
- if (!done) {
615
- promise = new Promise((resolve, reject) => {
616
- done = shared.callbackPromise(resolve, reject);
617
- });
618
- }
619
-
620
- this._responseActions.push(str => {
621
- lastResponse = str;
622
-
623
- let codes = str.match(/^(\d+)(?:\s(\d+\.\d+\.\d+))?\s/);
624
- let data = {
625
- command: cmd,
626
- response: str
627
- };
628
- if (codes) {
629
- data.status = Number(codes[1]) || 0;
630
- if (codes[2]) {
631
- data.code = codes[2];
632
- }
633
- data.text = str.substr(codes[0].length);
634
- } else {
635
- data.text = str;
636
- data.status = 0; // just in case we need to perform numeric comparisons
637
- }
638
- done(null, data);
639
- });
640
- setImmediate(() => this._sendCommand(cmd));
641
-
642
- return promise;
643
- },
644
-
525
+ sendCommand: sendCommand,
645
526
  resolve,
646
527
  reject
647
528
  });
648
-
649
529
  if (handlerResponse && typeof handlerResponse.catch === 'function') {
650
530
  // a promise was returned
651
531
  handlerResponse.then(resolve).catch(reject);
652
532
  }
653
-
654
533
  return;
655
534
  }
656
-
657
535
  switch (this._authMethod) {
658
536
  case 'XOAUTH2':
659
537
  this._handleXOauth2Token(false, callback);
@@ -668,27 +546,21 @@ class SMTPConnection extends EventEmitter {
668
546
  this._responseActions.push(str => {
669
547
  this._actionAUTHComplete(str, callback);
670
548
  });
671
- this._sendCommand(
672
- 'AUTH PLAIN ' +
673
- Buffer.from(
674
- //this._auth.user+'\u0000'+
675
- '\u0000' + // skip authorization identity as it causes problems with some servers
676
- this._auth.credentials.user +
677
- '\u0000' +
678
- this._auth.credentials.pass,
679
- 'utf-8'
680
- ).toString('base64'),
681
- // log entry without passwords
682
- 'AUTH PLAIN ' +
683
- Buffer.from(
684
- //this._auth.user+'\u0000'+
685
- '\u0000' + // skip authorization identity as it causes problems with some servers
686
- this._auth.credentials.user +
687
- '\u0000' +
688
- '/* secret */',
689
- 'utf-8'
690
- ).toString('base64')
691
- );
549
+ this._sendCommand('AUTH PLAIN ' +
550
+ Buffer.from(
551
+ //this._auth.user+'\u0000'+
552
+ '\u0000' + // skip authorization identity as it causes problems with some servers
553
+ this._auth.credentials.user +
554
+ '\u0000' +
555
+ this._auth.credentials.pass, 'utf-8').toString('base64'),
556
+ // log entry without passwords
557
+ 'AUTH PLAIN ' +
558
+ Buffer.from(
559
+ //this._auth.user+'\u0000'+
560
+ '\u0000' + // skip authorization identity as it causes problems with some servers
561
+ this._auth.credentials.user +
562
+ '\u0000' +
563
+ '/* secret */', 'utf-8').toString('base64'));
692
564
  return;
693
565
  case 'CRAM-MD5':
694
566
  this._responseActions.push(str => {
@@ -697,49 +569,53 @@ class SMTPConnection extends EventEmitter {
697
569
  this._sendCommand('AUTH CRAM-MD5');
698
570
  return;
699
571
  }
700
-
701
572
  return callback(this._formatError('Unknown authentication method "' + this._authMethod + '"', 'EAUTH', false, 'API'));
702
573
  }
703
-
704
574
  /**
705
575
  * Sends a message
706
576
  *
707
- * @param {Object} envelope Envelope object, {from: addr, to: [addr]}
708
- * @param {Object} message String, Buffer or a Stream
709
- * @param {Function} callback Callback to return once sending is completed
577
+ * @param envelope Envelope object, {from: addr, to: [addr]}
578
+ * @param message String, Buffer or a Stream
579
+ * @param done Callback to return once sending is completed
710
580
  */
711
581
  send(envelope, message, done) {
582
+ // ensure that the callback is only called once. The public callback type has a
583
+ // required result, the error paths hand over the error alone
584
+ let returned = false;
585
+ const callback = (err, info) => {
586
+ if (returned) {
587
+ return;
588
+ }
589
+ returned = true;
590
+ if (this._pendingSend && this._pendingSend.callback === callback) {
591
+ this._pendingSend = false;
592
+ }
593
+ done(err, info);
594
+ };
712
595
  if (!message) {
713
- return done(this._formatError('Empty message', 'EMESSAGE', false, 'API'));
596
+ return callback(this._formatError('Empty message', 'EMESSAGE', false, 'API'));
714
597
  }
715
-
716
598
  const isDestroyedMessage = this._isDestroyedMessage('send message');
717
599
  if (isDestroyedMessage) {
718
- return done(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API'));
600
+ return callback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API'));
719
601
  }
720
-
721
602
  // reject larger messages than allowed
722
603
  if (this._maxAllowedSize && envelope.size > this._maxAllowedSize) {
723
- return setImmediate(() => {
724
- done(this._formatError('Message size larger than allowed ' + this._maxAllowedSize, 'EMESSAGE', false, 'MAIL FROM'));
604
+ setImmediate(() => {
605
+ callback(this._formatError('Message size larger than allowed ' + this._maxAllowedSize, 'EMESSAGE', false, 'MAIL FROM'));
725
606
  });
607
+ return;
726
608
  }
727
-
728
- // ensure that callback is only called once
729
- let returned = false;
730
- const callback = function () {
731
- if (returned) {
732
- return;
733
- }
734
- returned = true;
735
-
736
- done(...arguments);
609
+ const pendingSend = {
610
+ callback,
611
+ stream: false,
612
+ onStreamError: err => callback(this._formatError(err, 'ESTREAM', false, 'API'))
737
613
  };
738
-
739
614
  if (typeof message.on === 'function') {
740
- message.on('error', err => callback(this._formatError(err, 'ESTREAM', false, 'API')));
615
+ pendingSend.stream = message;
616
+ pendingSend.stream.on('error', pendingSend.onStreamError);
741
617
  }
742
-
618
+ this._pendingSend = pendingSend;
743
619
  const startTime = Date.now();
744
620
  this._setEnvelope(envelope, (err, info) => {
745
621
  if (err) {
@@ -747,11 +623,11 @@ class SMTPConnection extends EventEmitter {
747
623
  const stream = new PassThrough();
748
624
  if (typeof message.pipe === 'function') {
749
625
  message.pipe(stream);
750
- } else {
626
+ }
627
+ else {
751
628
  stream.write(message);
752
629
  stream.end();
753
630
  }
754
-
755
631
  return callback(err);
756
632
  }
757
633
  const envelopeTime = Date.now();
@@ -759,34 +635,33 @@ class SMTPConnection extends EventEmitter {
759
635
  if (err) {
760
636
  return callback(err);
761
637
  }
762
-
763
- info.envelopeTime = envelopeTime - startTime;
764
- info.messageTime = Date.now() - envelopeTime;
765
- info.messageSize = stream.outByteCount;
766
- info.response = str;
767
-
768
- return callback(null, info);
638
+ // the envelope info becomes the send result once the timings are on it
639
+ const result = info;
640
+ result.envelopeTime = envelopeTime - startTime;
641
+ result.messageTime = Date.now() - envelopeTime;
642
+ result.messageSize = stream.outByteCount;
643
+ result.response = str;
644
+ return callback(null, result);
769
645
  });
770
646
  if (typeof message.pipe === 'function') {
771
647
  message.pipe(stream);
772
- } else {
648
+ }
649
+ else {
773
650
  stream.write(message);
774
651
  stream.end();
775
652
  }
776
653
  });
777
654
  }
778
-
779
655
  /**
780
656
  * Resets connection state
781
657
  *
782
- * @param {Function} callback Callback to return once connection is reset
658
+ * @param callback Callback to return once connection is reset
783
659
  */
784
660
  reset(callback) {
785
661
  const isDestroyedMessage = this._isDestroyedMessage('reset');
786
662
  if (isDestroyedMessage) {
787
663
  return callback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API'));
788
664
  }
789
-
790
665
  this._sendCommand('RSET');
791
666
  this._responseActions.push(str => {
792
667
  if (str.charAt(0) !== '2') {
@@ -796,231 +671,245 @@ class SMTPConnection extends EventEmitter {
796
671
  return callback(null, true);
797
672
  });
798
673
  }
799
-
800
674
  /**
801
675
  * Connection listener that is run when the connection to
802
676
  * the server is opened
803
677
  *
804
678
  * @event
679
+ * @internal
805
680
  */
806
681
  _onConnect() {
682
+ const socket = this._socket;
807
683
  clearTimeout(this._connectionTimeout);
808
-
809
- this.logger.info(
810
- {
811
- tnx: 'network',
812
- localAddress: this._socket.localAddress,
813
- localPort: this._socket.localPort,
814
- remoteAddress: this._socket.remoteAddress,
815
- remotePort: this._socket.remotePort
816
- },
817
- '%s established to %s:%s',
818
- this.secure ? 'Secure connection' : 'Connection',
819
- this._socket.remoteAddress,
820
- this._socket.remotePort
821
- );
822
-
684
+ this.logger.info({
685
+ tnx: 'network',
686
+ localAddress: socket.localAddress,
687
+ localPort: socket.localPort,
688
+ remoteAddress: socket.remoteAddress,
689
+ remotePort: socket.remotePort
690
+ }, '%s established to %s:%s', this.secure ? 'Secure connection' : 'Connection', socket.remoteAddress, socket.remotePort);
823
691
  if (this._destroyed) {
824
692
  // Connection was established after we already had canceled it
825
693
  this.close();
826
694
  return;
827
695
  }
828
-
829
696
  this.stage = 'connected';
830
-
831
697
  // clear existing listeners for the socket
832
- this._socket.removeListener('data', this._onSocketData);
833
- this._socket.removeListener('timeout', this._onSocketTimeout);
834
- this._socket.removeListener('close', this._onSocketClose);
835
- this._socket.removeListener('end', this._onSocketEnd);
698
+ socket.removeListener('data', this._onSocketData);
699
+ socket.removeListener('timeout', this._onSocketTimeout);
700
+ socket.removeListener('close', this._onSocketClose);
701
+ socket.removeListener('end', this._onSocketEnd);
836
702
  // Switch from connection-phase error handler to normal error handler
837
- this._socket.removeListener('error', this._onConnectionSocketError);
703
+ socket.removeListener('error', this._onConnectionSocketError);
838
704
  // _upgradeConnection (options.connection + secure) may already have attached
839
705
  // the normal handler; remove it first so we never end up with a duplicate
840
- this._socket.removeListener('error', this._onSocketError);
841
-
842
- this._socket.on('error', this._onSocketError);
843
- this._socket.on('data', this._onSocketData);
844
- this._socket.once('close', this._onSocketClose);
845
- this._socket.once('end', this._onSocketEnd);
846
-
847
- this._socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT);
848
- this._socket.on('timeout', this._onSocketTimeout);
849
-
706
+ socket.removeListener('error', this._onSocketError);
707
+ socket.on('error', this._onSocketError);
708
+ socket.on('data', this._onSocketData);
709
+ socket.once('close', this._onSocketClose);
710
+ socket.once('end', this._onSocketEnd);
711
+ socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT);
712
+ socket.on('timeout', this._onSocketTimeout);
850
713
  this._greetingTimeout = setTimeout(() => {
851
714
  // if still waiting for greeting, give up
852
715
  if (this._socket && !this._destroyed && this._responseActions[0] === this._actionGreeting) {
853
716
  this._onError('Greeting never received', 'ETIMEDOUT', false, 'CONN');
854
717
  }
855
718
  }, this.options.greetingTimeout || GREETING_TIMEOUT);
856
-
857
719
  this._responseActions.push(this._actionGreeting);
858
-
859
720
  // we have a 'data' listener set up so resume socket if it was paused
860
- this._socket.resume();
721
+ socket.resume();
861
722
  }
862
-
863
723
  /**
864
724
  * 'data' listener for data coming from the server
865
725
  *
866
726
  * @event
867
- * @param {Buffer} chunk Data chunk coming from the server
727
+ * @param chunk Data chunk coming from the server
728
+ * @internal
868
729
  */
869
730
  _onData(chunk) {
870
731
  if (this._destroyed || !chunk || !chunk.length) {
871
732
  return;
872
733
  }
873
-
874
- let data = chunk.toString('binary');
875
- let lines = (this._remainder + data).split(/\r?\n/);
876
- let lastline;
877
-
734
+ const maxResponseSize = this.options.maxResponseSize || MAX_RESPONSE_SIZE;
735
+ const data = chunk.toString('binary');
736
+ // A chunk without a line break only extends the line currently being received, so
737
+ // keep it in the remainder and leave that string unflattened. Splitting the whole
738
+ // remainder again on every chunk would rescan everything buffered for that line so
739
+ // far, which is quadratic in the length of a line the peer never terminates
740
+ if (!data.includes('\n')) {
741
+ this._remainder += data;
742
+ if (this._remainder.length > maxResponseSize) {
743
+ return this._onResponseTooLarge();
744
+ }
745
+ return;
746
+ }
747
+ const lines = (this._remainder + data).split(/\r?\n/);
878
748
  this._remainder = lines.pop();
879
-
880
749
  for (let i = 0, len = lines.length; i < len; i++) {
881
- if (this._responseQueue.length) {
882
- lastline = this._responseQueue[this._responseQueue.length - 1];
883
- if (/^\d+-/.test(lastline.split('\n').pop())) {
884
- this._responseQueue[this._responseQueue.length - 1] += '\n' + lines[i];
885
- continue;
886
- }
750
+ if (this._responsePartial) {
751
+ this._responseQueue[this._responseQueue.length - 1] += '\n' + lines[i];
887
752
  }
888
- this._responseQueue.push(lines[i]);
889
- }
890
-
891
- if (this._responseQueue.length) {
892
- lastline = this._responseQueue[this._responseQueue.length - 1];
893
- if (/^\d+-/.test(lastline.split('\n').pop())) {
894
- return;
753
+ else {
754
+ this._responseQueue.push(lines[i]);
755
+ }
756
+ // The line just added is the last line of that queue entry, so it alone decides
757
+ // whether the reply is still partial. Looking for the last line of the accumulated
758
+ // entry instead would rescan a string that grows with every continuation line,
759
+ // which is quadratic in the size of the reply
760
+ this._responsePartial = isPartialLine(lines[i]);
761
+ // Checked as each line lands, so a peer that never completes a reply cannot keep
762
+ // buffering, and the limit does not depend on how it split its bytes into chunks
763
+ if (this._responsePartial && this._responseQueue[this._responseQueue.length - 1].length > maxResponseSize) {
764
+ return this._onResponseTooLarge();
895
765
  }
896
766
  }
897
-
767
+ if (this._remainder.length > maxResponseSize) {
768
+ return this._onResponseTooLarge();
769
+ }
770
+ if (this._responsePartial) {
771
+ return;
772
+ }
898
773
  this._processResponse();
899
774
  }
900
-
775
+ /**
776
+ * Drops a connection whose peer keeps extending a reply it never completes, releasing
777
+ * whatever was buffered for that reply
778
+ * @internal
779
+ */
780
+ _onResponseTooLarge() {
781
+ this._remainder = '';
782
+ this._responseQueue = [];
783
+ this._responsePartial = false;
784
+ this._onError(new Error('Server response exceeds maximum allowed size'), 'EPROTOCOL', false, 'CONN');
785
+ }
901
786
  /**
902
787
  * 'error' listener for the socket
903
788
  *
904
789
  * @event
905
- * @param {Error} err Error object
906
- * @param {String} type Error name
790
+ * @param err Error object
791
+ * @param type Error name
792
+ * @param data Server response that triggered the error, false if there is none
793
+ * @param command SMTP command that was in flight
794
+ * @internal
907
795
  */
908
796
  _onError(err, type, data, command) {
909
797
  clearTimeout(this._connectionTimeout);
910
798
  clearTimeout(this._greetingTimeout);
911
-
912
799
  if (this._destroyed) {
913
800
  // just ignore, already closed
914
801
  // this might happen when a socket is canceled because of reached timeout
915
802
  // but the socket timeout error itself receives only after
916
803
  return;
917
804
  }
918
-
919
805
  err = this._formatError(err, type, data, command);
920
-
921
806
  const transientCodes = ['ETIMEDOUT', 'ESOCKET', 'ECONNECTION'];
922
807
  if (transientCodes.includes(err.code)) {
923
808
  this.logger.warn(data, err.message);
924
- } else {
809
+ }
810
+ else {
925
811
  this.logger.error(data, err.message);
926
812
  }
927
-
813
+ // close() forgets the send in flight, it is completed with this same error afterwards so
814
+ // a late message stream error has nothing left to report
815
+ const pendingSend = this._pendingSend;
928
816
  this.emit('error', err);
929
817
  this.close();
818
+ if (pendingSend) {
819
+ pendingSend.callback(err);
820
+ }
930
821
  }
931
-
822
+ /** @internal */
932
823
  _formatError(message, type, response, command) {
933
824
  let err;
934
-
935
825
  if (/Error\]$/i.test(Object.prototype.toString.call(message))) {
936
826
  err = message;
937
- } else {
827
+ }
828
+ else {
938
829
  err = new Error(message);
939
830
  }
940
-
941
831
  if (type && type !== 'Error') {
942
832
  err.code = type;
943
833
  }
944
-
945
834
  if (response) {
946
835
  err.response = response;
947
836
  err.message += ': ' + response;
948
837
  }
949
-
950
838
  const responseCode = (typeof response === 'string' && Number((response.match(/^\d+/) || [])[0])) || false;
951
839
  if (responseCode) {
952
840
  err.responseCode = responseCode;
953
841
  }
954
-
955
842
  if (command) {
956
843
  err.command = command;
957
844
  }
958
-
959
845
  return err;
960
846
  }
961
-
962
847
  /**
963
848
  * 'close' listener for the socket
964
849
  *
965
850
  * @event
851
+ * @internal
966
852
  */
967
853
  _onClose() {
968
854
  let serverResponse = false;
969
-
970
855
  if (this._remainder && this._remainder.trim()) {
971
856
  this.lastServerResponse = serverResponse = decodeServerResponse(this._remainder.trim());
972
857
  if (this.options.debug || this.options.transactionLog) {
973
- this.logger.debug(
974
- {
975
- tnx: 'server'
976
- },
977
- serverResponse
978
- );
858
+ this.logger.debug({
859
+ tnx: 'server'
860
+ }, serverResponse);
979
861
  }
980
862
  }
981
-
982
- this.logger.info(
983
- {
984
- tnx: 'network'
985
- },
986
- 'Connection closed'
987
- );
988
-
863
+ this.logger.info({
864
+ tnx: 'network'
865
+ }, 'Connection closed');
866
+ // the unterminated remainder is only reported as a reply (and so gives the error a responseCode)
867
+ // when it starts like a complete failure reply, not for a fragment such as "55" or a 250
868
+ const failureResponse = typeof serverResponse === 'string' && /^[45]\d{2}[ -]/.test(serverResponse) ? serverResponse : false;
989
869
  if (this.upgrading && !this._destroyed) {
990
- return this._onError(new Error('Connection closed unexpectedly'), 'ETLS', serverResponse, 'CONN');
991
- } else if (![this._actionGreeting, this.close].includes(this._responseActions[0]) && !this._destroyed) {
992
- return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', serverResponse, 'CONN');
993
- } else if (/^[45]\d{2}\b/.test(serverResponse)) {
994
- return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', serverResponse, 'CONN');
870
+ return this._onError(new Error('Connection closed unexpectedly'), 'ETLS', failureResponse, 'CONN');
871
+ }
872
+ if (!failureResponse && this._responseActions[0] === this._actionGreeting && this._connectCallback && !this._destroyed) {
873
+ // A silent close before the greeting is handed to the connect() callback rather than
874
+ // emitted as 'error', callers that never saw an error for it must not start throwing one
875
+ const connectCallback = this._connectCallback;
876
+ this._connectCallback = false;
877
+ const err = this._formatError(new Error('Connection closed unexpectedly'), 'ECONNECTION', false, 'CONN');
878
+ this.logger.warn({ tnx: 'network' }, err.message);
879
+ connectCallback(err);
880
+ this.close();
881
+ return;
882
+ }
883
+ if (failureResponse || (this._responseActions[0] !== this.close && !this._destroyed)) {
884
+ return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', failureResponse, 'CONN');
995
885
  }
996
-
997
886
  this._destroy();
998
887
  }
999
-
1000
888
  /**
1001
889
  * 'end' listener for the socket
1002
890
  *
1003
891
  * @event
892
+ * @internal
1004
893
  */
1005
894
  _onEnd() {
1006
895
  if (this._socket && !this._socket.destroyed) {
1007
- // Peer sent FIN — finish our half of the close gracefully rather
896
+ // Peer sent FIN, finish our half of the close gracefully rather
1008
897
  // than destroying. 'close' fires after the OS finalizes teardown.
1009
898
  this._socket.end();
1010
899
  }
1011
900
  }
1012
-
1013
901
  /**
1014
902
  * 'timeout' listener for the socket
1015
903
  *
1016
904
  * @event
905
+ * @internal
1017
906
  */
1018
907
  _onTimeout() {
1019
908
  return this._onError(new Error('Timeout'), 'ETIMEDOUT', false, 'CONN');
1020
909
  }
1021
-
1022
910
  /**
1023
911
  * Destroys the client, emits 'end'
912
+ * @internal
1024
913
  */
1025
914
  _destroy() {
1026
915
  if (this._destroyed) {
@@ -1029,14 +918,20 @@ class SMTPConnection extends EventEmitter {
1029
918
  this._destroyed = true;
1030
919
  // keep the documented public flag in sync with the private state
1031
920
  this.destroyed = true;
921
+ // a connection the server dropped before the greeting would otherwise keep
922
+ // the greeting timer, and with it the process, alive until it fires
923
+ clearTimeout(this._connectionTimeout);
924
+ clearTimeout(this._greetingTimeout);
925
+ this._connectionTimeout = false;
926
+ this._greetingTimeout = false;
1032
927
  this.emit('end');
1033
928
  }
1034
-
1035
929
  /**
1036
930
  * Upgrades the connection to TLS
1037
931
  *
1038
- * @param {Function} callback Callback function to run when the connection
932
+ * @param callback Callback function to run when the connection
1039
933
  * has been secured
934
+ * @internal
1040
935
  */
1041
936
  _upgradeConnection(callback) {
1042
937
  // RFC 3207 section 6: the client MUST discard any knowledge obtained from
@@ -1047,28 +942,21 @@ class SMTPConnection extends EventEmitter {
1047
942
  // part of the secured EHLO capabilities). STARTTLS response injection.
1048
943
  this._remainder = '';
1049
944
  this._responseQueue = [];
1050
-
945
+ this._responsePartial = false;
1051
946
  // do not remove all listeners or it breaks node v0.10 as there's
1052
947
  // apparently a 'finish' event set that would be cleared as well
1053
-
1054
948
  // we can safely keep 'error', 'end', 'close' etc. events
1055
- this._socket.removeListener('data', this._onSocketData); // incoming data is going to be gibberish from this point onwards
1056
- this._socket.removeListener('timeout', this._onSocketTimeout); // timeout will be re-set for the new socket object
1057
-
1058
949
  const socketPlain = this._socket;
1059
- const opts = Object.assign(
1060
- {
1061
- socket: this._socket,
1062
- host: this.host
1063
- },
1064
- this.options.tls || {}
1065
- );
1066
-
950
+ socketPlain.removeListener('data', this._onSocketData); // incoming data is going to be gibberish from this point onwards
951
+ socketPlain.removeListener('timeout', this._onSocketTimeout); // timeout will be re-set for the new socket object
952
+ const opts = Object.assign({
953
+ socket: socketPlain,
954
+ host: this.host
955
+ }, this.options.tls || {});
1067
956
  // ensure servername for SNI
1068
957
  if (this.servername && !opts.servername) {
1069
958
  opts.servername = this.servername;
1070
959
  }
1071
-
1072
960
  // Remove all listeners from the plain socket to allow proper garbage
1073
961
  // collection. Used on both the TLS-success path and the synchronous
1074
962
  // tls.connect() throw path; either way the plain socket is done.
@@ -1080,7 +968,6 @@ class SMTPConnection extends EventEmitter {
1080
968
  // options.connection socket; strip it so nothing lingers on the plain socket
1081
969
  socketPlain.removeListener('error', this._onConnectionSocketError);
1082
970
  };
1083
-
1084
971
  this.upgrading = true;
1085
972
  // tls.connect is not an asynchronous function however it may still throw errors and requires to be wrapped with try/catch
1086
973
  try {
@@ -1088,200 +975,170 @@ class SMTPConnection extends EventEmitter {
1088
975
  this.secure = true;
1089
976
  this.upgrading = false;
1090
977
  this._socket.on('data', this._onSocketData);
1091
-
1092
978
  removePlainSocketListeners();
1093
-
1094
979
  return callback(null, true);
1095
980
  });
1096
- } catch (err) {
981
+ }
982
+ catch (err) {
1097
983
  removePlainSocketListeners();
1098
984
  return callback(err);
1099
985
  }
1100
-
1101
986
  this._socket.on('error', this._onSocketError);
1102
987
  this._socket.once('close', this._onSocketClose);
1103
988
  this._socket.once('end', this._onSocketEnd);
1104
-
1105
989
  this._socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT); // 10 min.
1106
990
  this._socket.on('timeout', this._onSocketTimeout);
1107
-
1108
991
  // resume in case the socket was paused
1109
992
  socketPlain.resume();
1110
993
  }
1111
-
1112
994
  /**
1113
995
  * Processes queued responses from the server
996
+ * @internal
1114
997
  */
1115
998
  _processResponse() {
1116
999
  if (!this._responseQueue.length) {
1117
1000
  return false;
1118
1001
  }
1119
-
1120
1002
  const raw = (this._responseQueue.shift() || '').toString();
1121
-
1122
1003
  // Skip unexpected empty lines without consuming a response action or
1123
1004
  // overwriting lastServerResponse; reprocess whatever else is queued.
1124
1005
  if (!raw.trim()) {
1125
1006
  setImmediate(() => this._processResponse());
1126
1007
  return;
1127
1008
  }
1128
-
1129
- let str = (this.lastServerResponse = decodeServerResponse(raw));
1130
-
1131
- if (/^\d+-/.test(str.split('\n').pop())) {
1132
- // last line is still a continuation: put the partial response back on the
1133
- // queue and wait for the rest rather than dropping it
1009
+ if (isPartialResponse(raw)) {
1010
+ // the rest of the reply is still on its way: put it back on the queue and wait
1011
+ // rather than dropping it. It has not been received in full, so it must not be
1012
+ // reported as the last server response either
1134
1013
  this._responseQueue.unshift(raw);
1135
1014
  return;
1136
1015
  }
1137
-
1016
+ const str = (this.lastServerResponse = decodeServerResponse(raw));
1138
1017
  if (this.options.debug || this.options.transactionLog) {
1139
- this.logger.debug(
1140
- {
1141
- tnx: 'server'
1142
- },
1143
- str.replace(/\r?\n$/, '')
1144
- );
1018
+ this.logger.debug({
1019
+ tnx: 'server'
1020
+ }, str.replace(/\r?\n$/, ''));
1145
1021
  }
1146
-
1147
1022
  const action = this._responseActions.shift();
1148
-
1149
1023
  if (typeof action === 'function') {
1150
1024
  action.call(this, str);
1151
1025
  setImmediate(() => this._processResponse());
1152
- } else {
1026
+ }
1027
+ else {
1153
1028
  return this._onError(new Error('Unexpected Response'), 'EPROTOCOL', str, 'CONN');
1154
1029
  }
1155
1030
  }
1156
-
1157
1031
  /**
1158
1032
  * Send a command to the server, append \r\n
1159
1033
  *
1160
- * @param {String} str String to be sent to the server
1161
- * @param {String} logStr Optional string to be used for logging instead of the actual string
1034
+ * @param str String to be sent to the server
1035
+ * @param logStr Optional string to be used for logging instead of the actual string
1036
+ * @internal
1162
1037
  */
1163
1038
  _sendCommand(str, logStr) {
1164
1039
  if (this._destroyed) {
1165
1040
  // Connection already closed, can't send any more data
1166
1041
  return;
1167
1042
  }
1168
-
1169
- if (this._socket.destroyed) {
1043
+ const socket = this._socket;
1044
+ if (socket.destroyed) {
1170
1045
  return this.close();
1171
1046
  }
1172
-
1173
1047
  if (this.options.debug || this.options.transactionLog) {
1174
- this.logger.debug(
1175
- {
1176
- tnx: 'client'
1177
- },
1178
- (logStr || str || '').toString().replace(/\r?\n$/, '')
1179
- );
1180
- }
1181
-
1182
- this._socket.write(Buffer.from(str + '\r\n', 'utf-8'));
1048
+ this.logger.debug({
1049
+ tnx: 'client'
1050
+ }, (logStr || str || '').toString().replace(/\r?\n$/, ''));
1051
+ }
1052
+ socket.write(Buffer.from(str + '\r\n', 'utf-8'));
1183
1053
  }
1184
-
1185
1054
  /**
1186
1055
  * Initiates a new message by submitting envelope data, starting with
1187
1056
  * MAIL FROM: command
1188
1057
  *
1189
- * @param {Object} envelope Envelope object in the form of
1058
+ * @param envelope Envelope object in the form of
1190
1059
  * {from:'...', to:['...']}
1191
1060
  * or
1192
1061
  * {from:{address:'...',name:'...'}, to:[address:'...',name:'...']}
1062
+ * @param callback Callback to run once the envelope is processed
1063
+ * @internal
1193
1064
  */
1194
1065
  _setEnvelope(envelope, callback) {
1195
1066
  const args = [];
1196
1067
  let useSmtpUtf8 = false;
1197
-
1198
- this._envelope = envelope || {};
1199
- this._envelope.from = ((this._envelope.from && this._envelope.from.address) || this._envelope.from || '').toString().trim();
1200
-
1201
- this._envelope.to = [].concat(this._envelope.to || []).map(to => ((to && to.address) || to || '').toString().trim());
1202
-
1068
+ this._envelope = (envelope || {});
1069
+ this._envelope.from = ((this._envelope.from && this._envelope.from.address) || this._envelope.from || '')
1070
+ .toString()
1071
+ .trim();
1072
+ this._envelope.to = []
1073
+ .concat(this._envelope.to || [])
1074
+ .map(to => ((to && to.address) || to || '').toString().trim());
1203
1075
  if (!this._envelope.to.length) {
1204
1076
  return callback(this._formatError('No recipients defined', 'EENVELOPE', false, 'API'));
1205
1077
  }
1206
-
1207
1078
  if (this._envelope.from && /[\r\n<>]/.test(this._envelope.from)) {
1208
1079
  return callback(this._formatError('Invalid sender ' + JSON.stringify(this._envelope.from), 'EENVELOPE', false, 'API'));
1209
1080
  }
1210
-
1211
1081
  // check if the sender address uses only ASCII characters,
1212
1082
  // otherwise require usage of SMTPUTF8 extension
1213
1083
  if (/[\x80-\uFFFF]/.test(this._envelope.from)) {
1214
1084
  useSmtpUtf8 = true;
1215
1085
  }
1216
-
1217
1086
  for (let i = 0, len = this._envelope.to.length; i < len; i++) {
1218
1087
  if (!this._envelope.to[i] || /[\r\n<>]/.test(this._envelope.to[i])) {
1219
1088
  return callback(this._formatError('Invalid recipient ' + JSON.stringify(this._envelope.to[i]), 'EENVELOPE', false, 'API'));
1220
1089
  }
1221
-
1222
1090
  // check if the recipients addresses use only ASCII characters,
1223
1091
  // otherwise require usage of SMTPUTF8 extension
1224
1092
  if (/[\x80-\uFFFF]/.test(this._envelope.to[i])) {
1225
1093
  useSmtpUtf8 = true;
1226
1094
  }
1227
1095
  }
1228
-
1229
1096
  // clone the recipients array for latter manipulation
1230
1097
  this._envelope.rcptQueue = [].concat(this._envelope.to || []);
1231
1098
  this._envelope.rejected = [];
1232
1099
  this._envelope.rejectedErrors = [];
1233
1100
  this._envelope.accepted = [];
1234
-
1235
1101
  if (this._envelope.dsn) {
1236
1102
  try {
1237
1103
  this._envelope.dsn = this._setDsnEnvelope(this._envelope.dsn);
1238
- } catch (err) {
1104
+ }
1105
+ catch (err) {
1239
1106
  return callback(this._formatError('Invalid DSN ' + err.message, 'EENVELOPE', false, 'API'));
1240
1107
  }
1241
1108
  }
1242
-
1243
1109
  // RFC 8689: validate REQUIRETLS eligibility before queuing the MAIL FROM
1244
1110
  // response action, so a rejection here cannot leave an orphaned action in
1245
1111
  // _responseActions (which would consume the next reply and desync a reused
1246
1112
  // connection).
1247
1113
  if (this._envelope.requireTLSExtensionEnabled) {
1248
1114
  if (!this.secure) {
1249
- return callback(
1250
- this._formatError('REQUIRETLS can only be used over TLS connections (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
1251
- );
1115
+ return callback(this._formatError('REQUIRETLS can only be used over TLS connections (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM'));
1252
1116
  }
1253
1117
  if (!this._supportedExtensions.includes('REQUIRETLS')) {
1254
- return callback(
1255
- this._formatError('Server does not support REQUIRETLS extension (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
1256
- );
1118
+ return callback(this._formatError('Server does not support REQUIRETLS extension (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM'));
1257
1119
  }
1258
1120
  }
1259
-
1260
1121
  this._responseActions.push(str => {
1261
1122
  this._actionMAIL(str, callback);
1262
1123
  });
1263
-
1264
1124
  // If the server supports SMTPUTF8 and the envelope includes an internationalized
1265
1125
  // email address then append SMTPUTF8 keyword to the MAIL FROM command
1266
1126
  if (useSmtpUtf8 && this._supportedExtensions.includes('SMTPUTF8')) {
1267
1127
  args.push('SMTPUTF8');
1268
1128
  this._usingSmtpUtf8 = true;
1269
1129
  }
1270
-
1271
1130
  // If the server supports 8BITMIME and the message might contain non-ascii bytes
1272
1131
  // then append the 8BITMIME keyword to the MAIL FROM command
1273
1132
  if (this._envelope.use8BitMime && this._supportedExtensions.includes('8BITMIME')) {
1274
1133
  args.push('BODY=8BITMIME');
1275
1134
  this._using8BitMime = true;
1276
1135
  }
1277
-
1278
1136
  if (this._envelope.size && this._supportedExtensions.includes('SIZE')) {
1279
1137
  const sizeValue = Number(this._envelope.size) || 0;
1280
1138
  if (sizeValue > 0) {
1281
1139
  args.push('SIZE=' + sizeValue);
1282
1140
  }
1283
1141
  }
1284
-
1285
1142
  // If the server supports DSN and the envelope includes an DSN prop
1286
1143
  // then append DSN params to the MAIL FROM command
1287
1144
  if (this._envelope.dsn && this._supportedExtensions.includes('DSN')) {
@@ -1292,17 +1149,15 @@ class SMTPConnection extends EventEmitter {
1292
1149
  args.push('ENVID=' + shared.encodeXText(this._envelope.dsn.envid));
1293
1150
  }
1294
1151
  }
1295
-
1296
1152
  // RFC 8689: append the REQUIRETLS keyword to MAIL FROM. Eligibility
1297
1153
  // (TLS connection + server support) was already validated above, before
1298
1154
  // the response action was queued.
1299
1155
  if (this._envelope.requireTLSExtensionEnabled) {
1300
1156
  args.push('REQUIRETLS');
1301
1157
  }
1302
-
1303
1158
  this._sendCommand('MAIL FROM:<' + this._envelope.from + '>' + (args.length ? ' ' + args.join(' ') : ''));
1304
1159
  }
1305
-
1160
+ /** @internal */
1306
1161
  _setDsnEnvelope(params) {
1307
1162
  let ret = (params.ret || params.return || '').toString().toUpperCase() || null;
1308
1163
  if (ret) {
@@ -1317,13 +1172,10 @@ class SMTPConnection extends EventEmitter {
1317
1172
  break;
1318
1173
  }
1319
1174
  }
1320
-
1321
1175
  if (ret && !['FULL', 'HDRS'].includes(ret)) {
1322
1176
  throw new Error('ret: ' + JSON.stringify(ret));
1323
1177
  }
1324
-
1325
1178
  const envid = (params.envid || params.id || '').toString() || null;
1326
-
1327
1179
  let notify = params.notify || null;
1328
1180
  if (notify) {
1329
1181
  if (typeof notify === 'string') {
@@ -1337,12 +1189,10 @@ class SMTPConnection extends EventEmitter {
1337
1189
  }
1338
1190
  notify = notify.join(',');
1339
1191
  }
1340
-
1341
1192
  let orcpt = (params.recipient || params.orcpt || '').toString() || null;
1342
1193
  if (orcpt && orcpt.indexOf(';') < 0) {
1343
1194
  orcpt = 'rfc822;' + orcpt;
1344
1195
  }
1345
-
1346
1196
  return {
1347
1197
  ret,
1348
1198
  envid,
@@ -1350,292 +1200,252 @@ class SMTPConnection extends EventEmitter {
1350
1200
  orcpt
1351
1201
  };
1352
1202
  }
1353
-
1203
+ /** @internal */
1354
1204
  _getDsnRcptToArgs() {
1205
+ const envelope = this._envelope;
1355
1206
  const args = [];
1356
1207
  // If the server supports DSN and the envelope includes an DSN prop
1357
1208
  // then append DSN params to the RCPT TO command
1358
- if (this._envelope.dsn && this._supportedExtensions.includes('DSN')) {
1359
- if (this._envelope.dsn.notify) {
1360
- args.push('NOTIFY=' + shared.encodeXText(this._envelope.dsn.notify));
1209
+ if (envelope.dsn && this._supportedExtensions.includes('DSN')) {
1210
+ if (envelope.dsn.notify) {
1211
+ args.push('NOTIFY=' + shared.encodeXText(envelope.dsn.notify));
1361
1212
  }
1362
- if (this._envelope.dsn.orcpt) {
1363
- args.push('ORCPT=' + shared.encodeXText(this._envelope.dsn.orcpt));
1213
+ if (envelope.dsn.orcpt) {
1214
+ args.push('ORCPT=' + shared.encodeXText(envelope.dsn.orcpt));
1364
1215
  }
1365
1216
  }
1366
1217
  return args.length ? ' ' + args.join(' ') : '';
1367
1218
  }
1368
-
1219
+ /** @internal */
1369
1220
  _createSendStream(callback) {
1221
+ const envelope = this._envelope;
1370
1222
  const dataStream = new DataStream();
1371
-
1372
1223
  if (this.options.lmtp) {
1373
- this._envelope.accepted.forEach((recipient, i) => {
1374
- const final = i === this._envelope.accepted.length - 1;
1224
+ envelope.accepted.forEach((recipient, i) => {
1225
+ const final = i === envelope.accepted.length - 1;
1375
1226
  this._responseActions.push(str => {
1376
1227
  this._actionLMTPStream(recipient, final, str, callback);
1377
1228
  });
1378
1229
  });
1379
- } else {
1230
+ }
1231
+ else {
1380
1232
  this._responseActions.push(str => {
1381
1233
  this._actionSMTPStream(str, callback);
1382
1234
  });
1383
1235
  }
1384
-
1385
1236
  this._currentDataStream = dataStream;
1386
1237
  dataStream.pipe(this._socket, {
1387
1238
  end: false
1388
1239
  });
1389
-
1390
1240
  if (this.options.debug) {
1391
1241
  const logStream = new PassThrough();
1392
1242
  logStream.on('readable', () => {
1393
1243
  let chunk;
1394
1244
  while ((chunk = logStream.read())) {
1395
- this.logger.debug(
1396
- {
1397
- tnx: 'message'
1398
- },
1399
- chunk.toString('binary').replace(/\r?\n$/, '')
1400
- );
1245
+ this.logger.debug({
1246
+ tnx: 'message'
1247
+ }, chunk.toString('binary').replace(/\r?\n$/, ''));
1401
1248
  }
1402
1249
  });
1403
1250
  dataStream.pipe(logStream);
1404
1251
  }
1405
-
1406
1252
  dataStream.once('end', () => {
1407
1253
  if (this._currentDataStream === dataStream) {
1408
1254
  this._currentDataStream = false;
1409
1255
  }
1410
- this.logger.info(
1411
- {
1412
- tnx: 'message',
1413
- inByteCount: dataStream.inByteCount,
1414
- outByteCount: dataStream.outByteCount
1415
- },
1416
- '<%s bytes encoded mime message (source size %s bytes)>',
1417
- dataStream.outByteCount,
1418
- dataStream.inByteCount
1419
- );
1256
+ this.logger.info({
1257
+ tnx: 'message',
1258
+ inByteCount: dataStream.inByteCount,
1259
+ outByteCount: dataStream.outByteCount
1260
+ }, '<%s bytes encoded mime message (source size %s bytes)>', dataStream.outByteCount, dataStream.inByteCount);
1420
1261
  });
1421
-
1422
1262
  return dataStream;
1423
1263
  }
1424
-
1425
1264
  /** ACTIONS **/
1426
-
1427
1265
  /**
1428
1266
  * Will be run after the connection is created and the server sends
1429
1267
  * a greeting. If the incoming message starts with 220 initiate
1430
1268
  * SMTP session by sending EHLO command
1431
1269
  *
1432
- * @param {String} str Message from the server
1270
+ * @param str Message from the server
1271
+ * @internal
1433
1272
  */
1434
1273
  _actionGreeting(str) {
1435
1274
  clearTimeout(this._greetingTimeout);
1436
-
1437
1275
  if (str.substr(0, 3) !== '220') {
1438
1276
  this._onError(new Error('Invalid greeting. response=' + str), 'EPROTOCOL', str, 'CONN');
1439
1277
  return;
1440
1278
  }
1441
-
1442
1279
  if (this.options.lmtp) {
1443
1280
  this._responseActions.push(this._actionLHLO);
1444
1281
  this._sendCommand('LHLO ' + this.name);
1445
- } else {
1282
+ }
1283
+ else {
1446
1284
  this._responseActions.push(this._actionEHLO);
1447
1285
  this._sendCommand('EHLO ' + this.name);
1448
1286
  }
1449
1287
  }
1450
-
1451
1288
  /**
1452
1289
  * Handles server response for LHLO command. If it yielded in
1453
1290
  * error, emit 'error', otherwise treat this as an EHLO response
1454
1291
  *
1455
- * @param {String} str Message from the server
1292
+ * @param str Message from the server
1293
+ * @internal
1456
1294
  */
1457
1295
  _actionLHLO(str) {
1458
1296
  if (str.charAt(0) !== '2') {
1459
1297
  this._onError(new Error('Invalid LHLO. response=' + str), 'EPROTOCOL', str, 'LHLO');
1460
1298
  return;
1461
1299
  }
1462
-
1463
1300
  this._actionEHLO(str);
1464
1301
  }
1465
-
1466
1302
  /**
1467
1303
  * Handles server response for EHLO command. If it yielded in
1468
1304
  * error, try HELO instead, otherwise initiate TLS negotiation
1469
1305
  * if STARTTLS is supported by the server or move into the
1470
1306
  * authentication phase.
1471
1307
  *
1472
- * @param {String} str Message from the server
1308
+ * @param str Message from the server
1309
+ * @internal
1473
1310
  */
1474
1311
  _actionEHLO(str) {
1475
1312
  let match;
1476
-
1477
1313
  if (str.substr(0, 3) === '421') {
1478
1314
  this._onError(new Error('Server terminates connection. response=' + str), 'ECONNECTION', str, 'EHLO');
1479
1315
  return;
1480
1316
  }
1481
-
1482
1317
  if (str.charAt(0) !== '2') {
1483
1318
  if (this.options.requireTLS) {
1484
- this._onError(
1485
- new Error('EHLO failed but HELO does not support required STARTTLS. response=' + str),
1486
- 'ECONNECTION',
1487
- str,
1488
- 'EHLO'
1489
- );
1319
+ this._onError(new Error('EHLO failed but HELO does not support required STARTTLS. response=' + str), 'ECONNECTION', str, 'EHLO');
1490
1320
  return;
1491
1321
  }
1492
-
1493
1322
  // Try HELO instead
1494
1323
  this._responseActions.push(this._actionHELO);
1495
1324
  this._sendCommand('HELO ' + this.name);
1496
1325
  return;
1497
1326
  }
1498
-
1499
1327
  this._ehloLines = str
1500
1328
  .split(/\r?\n/)
1501
1329
  .map(line => line.replace(/^\d+[ -]/, '').trim())
1502
1330
  .filter(line => line)
1503
1331
  .slice(1);
1504
-
1505
1332
  // Detect if the server supports STARTTLS
1506
1333
  if (!this.secure && !this.options.ignoreTLS && (/[ -]STARTTLS\b/im.test(str) || this.options.requireTLS)) {
1507
1334
  this._sendCommand('STARTTLS');
1508
1335
  this._responseActions.push(this._actionSTARTTLS);
1509
1336
  return;
1510
1337
  }
1511
-
1512
1338
  // Detect if the server supports SMTPUTF8
1513
1339
  if (/[ -]SMTPUTF8\b/im.test(str)) {
1514
1340
  this._supportedExtensions.push('SMTPUTF8');
1515
1341
  }
1516
-
1517
1342
  // Detect if the server supports DSN
1518
1343
  if (/[ -]DSN\b/im.test(str)) {
1519
1344
  this._supportedExtensions.push('DSN');
1520
1345
  }
1521
-
1522
1346
  // Detect if the server supports 8BITMIME
1523
1347
  if (/[ -]8BITMIME\b/im.test(str)) {
1524
1348
  this._supportedExtensions.push('8BITMIME');
1525
1349
  }
1526
-
1527
1350
  // Detect if the server supports REQUIRETLS (RFC 8689)
1528
1351
  if (/[ -]REQUIRETLS\b/im.test(str)) {
1529
1352
  this._supportedExtensions.push('REQUIRETLS');
1530
1353
  }
1531
-
1532
1354
  // Detect if the server supports PIPELINING
1533
1355
  if (/[ -]PIPELINING\b/im.test(str)) {
1534
1356
  this._supportedExtensions.push('PIPELINING');
1535
1357
  }
1536
-
1537
1358
  // Detect if the server supports AUTH
1538
1359
  if (/[ -]AUTH\b/i.test(str)) {
1539
1360
  this.allowsAuth = true;
1540
1361
  }
1541
-
1542
- // Detect if the server supports PLAIN auth
1543
- if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)PLAIN/i.test(str)) {
1544
- this._supportedAuth.push('PLAIN');
1545
- }
1546
-
1547
- // Detect if the server supports LOGIN auth
1548
- if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)LOGIN/i.test(str)) {
1549
- this._supportedAuth.push('LOGIN');
1550
- }
1551
-
1552
- // Detect if the server supports CRAM-MD5 auth
1553
- if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)CRAM-MD5/i.test(str)) {
1554
- this._supportedAuth.push('CRAM-MD5');
1362
+ // Detect the advertised SASL mechanisms. The list is split into whole tokens rather
1363
+ // than searched for each name with a pattern: the patterns this replaced let two
1364
+ // whitespace runs overlap and backtracked quadratically over an AUTH line padded with
1365
+ // spaces, so a hostile server could stall the event loop from its EHLO reply
1366
+ // (GHSA-4ffr-jq9g-5ffx).
1367
+ const authMechanisms = new Set();
1368
+ for (const line of this._ehloLines) {
1369
+ const authMatch = /^AUTH[\s=](.*)/i.exec(line);
1370
+ if (authMatch) {
1371
+ for (const mechanism of authMatch[1].split(/[\s=]+/)) {
1372
+ authMechanisms.add(mechanism.toUpperCase());
1373
+ }
1374
+ }
1555
1375
  }
1556
-
1557
- // Detect if the server supports XOAUTH2 auth
1558
- if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)XOAUTH2/i.test(str)) {
1559
- this._supportedAuth.push('XOAUTH2');
1376
+ // listed in order of preference, the first one the credentials allow is used
1377
+ for (const mechanism of ['PLAIN', 'LOGIN', 'CRAM-MD5', 'XOAUTH2']) {
1378
+ if (authMechanisms.has(mechanism)) {
1379
+ this._supportedAuth.push(mechanism);
1380
+ }
1560
1381
  }
1561
-
1562
1382
  // Detect if the server supports SIZE extensions (and the max allowed size)
1563
1383
  if ((match = str.match(/[ -]SIZE(?:[ \t]+(\d+))?/im))) {
1564
1384
  this._supportedExtensions.push('SIZE');
1565
1385
  this._maxAllowedSize = Number(match[1]) || 0;
1566
1386
  }
1567
-
1568
1387
  this.emit('connect');
1569
1388
  }
1570
-
1571
1389
  /**
1572
1390
  * Handles server response for HELO command. If it yielded in
1573
1391
  * error, emit 'error', otherwise move into the authentication phase.
1574
1392
  *
1575
- * @param {String} str Message from the server
1393
+ * @param str Message from the server
1394
+ * @internal
1576
1395
  */
1577
1396
  _actionHELO(str) {
1578
1397
  if (str.charAt(0) !== '2') {
1579
1398
  this._onError(new Error('Invalid HELO. response=' + str), 'EPROTOCOL', str, 'HELO');
1580
1399
  return;
1581
1400
  }
1582
-
1583
1401
  // assume that authentication is enabled (most probably is not though)
1584
1402
  this.allowsAuth = true;
1585
-
1586
1403
  this.emit('connect');
1587
1404
  }
1588
-
1589
1405
  /**
1590
1406
  * Handles server response for STARTTLS command. If there's an error
1591
1407
  * try HELO instead, otherwise initiate TLS upgrade. If the upgrade
1592
1408
  * succeedes restart the EHLO
1593
1409
  *
1594
- * @param {String} str Message from the server
1410
+ * @param str Message from the server
1411
+ * @internal
1595
1412
  */
1596
1413
  _actionSTARTTLS(str) {
1597
1414
  if (str.charAt(0) !== '2') {
1598
1415
  if (this.options.opportunisticTLS) {
1599
- this.logger.info(
1600
- {
1601
- tnx: 'smtp'
1602
- },
1603
- 'Failed STARTTLS upgrade, continuing unencrypted'
1604
- );
1605
- return this.emit('connect');
1416
+ this.logger.info({
1417
+ tnx: 'smtp'
1418
+ }, 'Failed STARTTLS upgrade, continuing unencrypted');
1419
+ this.emit('connect');
1420
+ return;
1606
1421
  }
1607
1422
  this._onError(new Error('Error upgrading connection with STARTTLS'), 'ETLS', str, 'STARTTLS');
1608
1423
  return;
1609
1424
  }
1610
-
1611
1425
  this._upgradeConnection((err, secured) => {
1612
1426
  if (err) {
1613
1427
  this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'STARTTLS');
1614
1428
  return;
1615
1429
  }
1616
-
1617
- this.logger.info(
1618
- {
1619
- tnx: 'smtp'
1620
- },
1621
- 'Connection upgraded with STARTTLS'
1622
- );
1623
-
1430
+ this.logger.info({
1431
+ tnx: 'smtp'
1432
+ }, 'Connection upgraded with STARTTLS');
1624
1433
  if (secured) {
1625
1434
  // restart session
1626
1435
  if (this.options.lmtp) {
1627
1436
  this._responseActions.push(this._actionLHLO);
1628
1437
  this._sendCommand('LHLO ' + this.name);
1629
- } else {
1438
+ }
1439
+ else {
1630
1440
  this._responseActions.push(this._actionEHLO);
1631
1441
  this._sendCommand('EHLO ' + this.name);
1632
1442
  }
1633
- } else {
1443
+ }
1444
+ else {
1634
1445
  this.emit('connect');
1635
1446
  }
1636
1447
  });
1637
1448
  }
1638
-
1639
1449
  /**
1640
1450
  * Handle the response for AUTH LOGIN command. We are expecting
1641
1451
  * '334 VXNlcm5hbWU6' (base64 for 'Username:'). Data to be sent as
@@ -1643,7 +1453,9 @@ class SMTPConnection extends EventEmitter {
1643
1453
  * exact match but settle with 334 response in general as some
1644
1454
  * hosts invalidly use a longer message than VXNlcm5hbWU6
1645
1455
  *
1646
- * @param {String} str Message from the server
1456
+ * @param str Message from the server
1457
+ * @param callback Callback to run once the authentication sequence completes
1458
+ * @internal
1647
1459
  */
1648
1460
  _actionAUTH_LOGIN_USER(str, callback) {
1649
1461
  if (!/^334[ -]/.test(str)) {
@@ -1651,14 +1463,11 @@ class SMTPConnection extends EventEmitter {
1651
1463
  callback(this._formatError('Invalid login sequence while waiting for "334 VXNlcm5hbWU6"', 'EAUTH', str, 'AUTH LOGIN'));
1652
1464
  return;
1653
1465
  }
1654
-
1655
1466
  this._responseActions.push(str => {
1656
1467
  this._actionAUTH_LOGIN_PASS(str, callback);
1657
1468
  });
1658
-
1659
1469
  this._sendCommand(Buffer.from(this._auth.credentials.user + '', 'utf-8').toString('base64'));
1660
1470
  }
1661
-
1662
1471
  /**
1663
1472
  * Handle the response for AUTH CRAM-MD5 command. We are expecting
1664
1473
  * '334 <challenge string>'. Data to be sent as response needs to be
@@ -1666,102 +1475,89 @@ class SMTPConnection extends EventEmitter {
1666
1475
  * a HMAC key, prefixed by the username and a space, and finally all
1667
1476
  * base64 encoded again.
1668
1477
  *
1669
- * @param {String} str Message from the server
1478
+ * @param str Message from the server
1479
+ * @param callback Callback to run once the authentication sequence completes
1480
+ * @internal
1670
1481
  */
1671
1482
  _actionAUTH_CRAM_MD5(str, callback) {
1672
1483
  const challengeMatch = str.match(/^334\s+(.+)$/);
1673
-
1674
1484
  if (!challengeMatch) {
1675
- return callback(
1676
- this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH CRAM-MD5')
1677
- );
1485
+ return callback(this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH CRAM-MD5'));
1678
1486
  }
1679
-
1680
1487
  // Decode from base64
1681
1488
  const base64decoded = Buffer.from(challengeMatch[1], 'base64').toString('ascii');
1682
1489
  const hmacMD5 = crypto.createHmac('md5', this._auth.credentials.pass);
1683
-
1684
1490
  hmacMD5.update(base64decoded);
1685
-
1686
1491
  const prepended = this._auth.credentials.user + ' ' + hmacMD5.digest('hex');
1687
-
1688
1492
  this._responseActions.push(str => {
1689
1493
  this._actionAUTH_CRAM_MD5_PASS(str, callback);
1690
1494
  });
1691
-
1692
- this._sendCommand(
1693
- Buffer.from(prepended).toString('base64'),
1694
- // hidden hash for logs
1695
- Buffer.from(this._auth.credentials.user + ' /* secret */').toString('base64')
1696
- );
1495
+ this._sendCommand(Buffer.from(prepended).toString('base64'),
1496
+ // hidden hash for logs
1497
+ Buffer.from(this._auth.credentials.user + ' /* secret */').toString('base64'));
1697
1498
  }
1698
-
1699
1499
  /**
1700
1500
  * Handles the response to CRAM-MD5 authentication, if there's no error,
1701
1501
  * the user can be considered logged in. Start waiting for a message to send
1702
1502
  *
1703
- * @param {String} str Message from the server
1503
+ * @param str Message from the server
1504
+ * @param callback Callback to run once the authentication sequence completes
1505
+ * @internal
1704
1506
  */
1705
1507
  _actionAUTH_CRAM_MD5_PASS(str, callback) {
1706
1508
  if (!str.match(/^235\s+/)) {
1707
1509
  return callback(this._formatError('Invalid login sequence while waiting for "235"', 'EAUTH', str, 'AUTH CRAM-MD5'));
1708
1510
  }
1709
-
1710
- this.logger.info(
1711
- {
1712
- tnx: 'smtp',
1713
- username: this._auth.user,
1714
- action: 'authenticated',
1715
- method: this._authMethod
1716
- },
1717
- 'User %s authenticated',
1718
- JSON.stringify(this._auth.user)
1719
- );
1511
+ this.logger.info({
1512
+ tnx: 'smtp',
1513
+ username: this._auth.user,
1514
+ action: 'authenticated',
1515
+ method: this._authMethod
1516
+ }, 'User %s authenticated', JSON.stringify(this._auth.user));
1720
1517
  this.authenticated = true;
1721
1518
  callback(null, true);
1722
1519
  }
1723
-
1724
1520
  /**
1725
1521
  * Handle the response for AUTH LOGIN command. We are expecting
1726
1522
  * '334 UGFzc3dvcmQ6' (base64 for 'Password:'). Data to be sent as
1727
1523
  * response needs to be base64 encoded password.
1728
1524
  *
1729
- * @param {String} str Message from the server
1525
+ * @param str Message from the server
1526
+ * @param callback Callback to run once the authentication sequence completes
1527
+ * @internal
1730
1528
  */
1731
1529
  _actionAUTH_LOGIN_PASS(str, callback) {
1732
1530
  if (!/^334[ -]/.test(str)) {
1733
1531
  // expecting '334 UGFzc3dvcmQ6'
1734
1532
  return callback(this._formatError('Invalid login sequence while waiting for "334 UGFzc3dvcmQ6"', 'EAUTH', str, 'AUTH LOGIN'));
1735
1533
  }
1736
-
1737
1534
  this._responseActions.push(str => {
1738
1535
  this._actionAUTHComplete(str, callback);
1739
1536
  });
1740
-
1741
- this._sendCommand(
1742
- Buffer.from((this._auth.credentials.pass || '').toString(), 'utf-8').toString('base64'),
1743
- // Hidden pass for logs
1744
- Buffer.from('/* secret */', 'utf-8').toString('base64')
1745
- );
1537
+ this._sendCommand(Buffer.from((this._auth.credentials.pass || '').toString(), 'utf-8').toString('base64'),
1538
+ // Hidden pass for logs
1539
+ Buffer.from('/* secret */', 'utf-8').toString('base64'));
1746
1540
  }
1747
-
1748
1541
  /**
1749
1542
  * Handles the response for authentication, if there's no error,
1750
1543
  * the user can be considered logged in. Start waiting for a message to send
1751
1544
  *
1752
- * @param {String} str Message from the server
1545
+ * @param str Message from the server
1546
+ * @param isRetry True if this is a retry after a failed login, or the callback itself
1547
+ * @param [callback] Callback to run once the authentication sequence completes
1548
+ * @internal
1753
1549
  */
1754
1550
  _actionAUTHComplete(str, isRetry, callback) {
1755
1551
  if (!callback && typeof isRetry === 'function') {
1756
1552
  callback = isRetry;
1757
1553
  isRetry = false;
1758
1554
  }
1759
-
1760
1555
  if (str.substr(0, 3) === '334') {
1761
1556
  this._responseActions.push(str => {
1762
1557
  if (isRetry || this._authMethod !== 'XOAUTH2') {
1763
1558
  this._actionAUTHComplete(str, true, callback);
1764
- } else {
1559
+ }
1560
+ else {
1765
1561
  // fetch a new OAuth2 access token
1766
1562
  setImmediate(() => this._handleXOauth2Token(true, callback));
1767
1563
  }
@@ -1769,103 +1565,98 @@ class SMTPConnection extends EventEmitter {
1769
1565
  this._sendCommand('');
1770
1566
  return;
1771
1567
  }
1772
-
1773
1568
  if (str.charAt(0) !== '2') {
1774
- this.logger.info(
1775
- {
1776
- tnx: 'smtp',
1777
- username: this._auth.user,
1778
- action: 'authfail',
1779
- method: this._authMethod
1780
- },
1781
- 'User %s failed to authenticate',
1782
- JSON.stringify(this._auth.user)
1783
- );
1784
- return callback(this._formatError('Invalid login', 'EAUTH', str, 'AUTH ' + this._authMethod));
1785
- }
1786
-
1787
- this.logger.info(
1788
- {
1569
+ this.logger.info({
1789
1570
  tnx: 'smtp',
1790
1571
  username: this._auth.user,
1791
- action: 'authenticated',
1572
+ action: 'authfail',
1792
1573
  method: this._authMethod
1793
- },
1794
- 'User %s authenticated',
1795
- JSON.stringify(this._auth.user)
1796
- );
1574
+ }, 'User %s failed to authenticate', JSON.stringify(this._auth.user));
1575
+ return callback(this._formatError('Invalid login', 'EAUTH', str, 'AUTH ' + this._authMethod));
1576
+ }
1577
+ this.logger.info({
1578
+ tnx: 'smtp',
1579
+ username: this._auth.user,
1580
+ action: 'authenticated',
1581
+ method: this._authMethod
1582
+ }, 'User %s authenticated', JSON.stringify(this._auth.user));
1797
1583
  this.authenticated = true;
1798
1584
  callback(null, true);
1799
1585
  }
1800
-
1801
1586
  /**
1802
1587
  * Handle response for a MAIL FROM: command
1803
1588
  *
1804
- * @param {String} str Message from the server
1589
+ * @param str Message from the server
1590
+ * @param callback Callback to run once the envelope is processed
1591
+ * @internal
1805
1592
  */
1806
1593
  _actionMAIL(str, callback) {
1594
+ const envelope = this._envelope;
1807
1595
  if (Number(str.charAt(0)) !== 2) {
1808
- const message =
1809
- this._usingSmtpUtf8 && /^550 /.test(str) && /[\x80-\uFFFF]/.test(this._envelope.from)
1810
- ? 'Internationalized mailbox name not allowed'
1811
- : 'Mail command failed';
1596
+ const message = this._usingSmtpUtf8 && /^550 /.test(str) && /[\x80-\uFFFF]/.test(envelope.from)
1597
+ ? 'Internationalized mailbox name not allowed'
1598
+ : 'Mail command failed';
1812
1599
  return callback(this._formatError(message, 'EENVELOPE', str, 'MAIL FROM'));
1813
1600
  }
1814
-
1815
- if (!this._envelope.rcptQueue.length) {
1601
+ if (!envelope.rcptQueue.length) {
1816
1602
  return callback(this._formatError("Can't send mail - no recipients defined", 'EENVELOPE', false, 'API'));
1817
1603
  }
1818
-
1819
1604
  this._recipientQueue = [];
1820
1605
  const usePipelining = this._supportedExtensions.includes('PIPELINING');
1821
-
1822
1606
  do {
1823
- const curRecipient = this._envelope.rcptQueue.shift();
1607
+ const curRecipient = envelope.rcptQueue.shift();
1824
1608
  this._recipientQueue.push(curRecipient);
1825
1609
  this._responseActions.push(str => {
1826
1610
  this._actionRCPT(str, callback);
1827
1611
  });
1828
1612
  this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
1829
- } while (usePipelining && this._envelope.rcptQueue.length);
1613
+ } while (usePipelining && envelope.rcptQueue.length);
1830
1614
  }
1831
-
1832
1615
  /**
1833
1616
  * Handle response for a RCPT TO: command
1834
1617
  *
1835
- * @param {String} str Message from the server
1618
+ * @param str Message from the server
1619
+ * @param callback Callback to run once the envelope is processed
1620
+ * @internal
1836
1621
  */
1837
1622
  _actionRCPT(str, callback) {
1623
+ const envelope = this._envelope;
1838
1624
  let err;
1839
1625
  const curRecipient = this._recipientQueue.shift();
1840
1626
  if (Number(str.charAt(0)) !== 2) {
1841
1627
  // this is a soft error
1842
- const message =
1843
- this._usingSmtpUtf8 && /^553 /.test(str) && /[\x80-\uFFFF]/.test(curRecipient)
1844
- ? 'Internationalized mailbox name not allowed'
1845
- : 'Recipient command failed';
1846
- this._envelope.rejected.push(curRecipient);
1628
+ const message = this._usingSmtpUtf8 && /^553 /.test(str) && /[\x80-\uFFFF]/.test(curRecipient)
1629
+ ? 'Internationalized mailbox name not allowed'
1630
+ : 'Recipient command failed';
1631
+ envelope.rejected.push(curRecipient);
1847
1632
  // store error for the failed recipient
1848
1633
  err = this._formatError(message, 'EENVELOPE', str, 'RCPT TO');
1849
1634
  err.recipient = curRecipient;
1850
- this._envelope.rejectedErrors.push(err);
1851
- } else {
1852
- this._envelope.accepted.push(curRecipient);
1635
+ envelope.rejectedErrors.push(err);
1636
+ }
1637
+ else {
1638
+ envelope.accepted.push(curRecipient);
1853
1639
  }
1854
-
1855
- if (!this._envelope.rcptQueue.length && !this._recipientQueue.length) {
1856
- if (this._envelope.rejected.length < this._envelope.to.length) {
1640
+ if (!envelope.rcptQueue.length && !this._recipientQueue.length) {
1641
+ if (envelope.rejected.length < envelope.to.length) {
1857
1642
  this._responseActions.push(str => {
1858
1643
  this._actionDATA(str, callback);
1859
1644
  });
1860
1645
  this._sendCommand('DATA');
1861
- } else {
1862
- err = this._formatError("Can't send mail - all recipients were rejected", 'EENVELOPE', str, 'RCPT TO');
1863
- err.rejected = this._envelope.rejected;
1864
- err.rejectedErrors = this._envelope.rejectedErrors;
1646
+ }
1647
+ else {
1648
+ // report a temporary rejection when there is one, taking the last reply would mark the
1649
+ // whole message as permanently failed although some recipients were only deferred
1650
+ const deferred = envelope.rejectedErrors.find(rejectedErr => rejectedErr.responseCode && rejectedErr.responseCode < 500);
1651
+ const reply = deferred?.response ?? str;
1652
+ err = this._formatError("Can't send mail - all recipients were rejected", 'EENVELOPE', reply, 'RCPT TO');
1653
+ err.rejected = envelope.rejected;
1654
+ err.rejectedErrors = envelope.rejectedErrors;
1865
1655
  return callback(err);
1866
1656
  }
1867
- } else if (this._envelope.rcptQueue.length) {
1868
- const nextRecipient = this._envelope.rcptQueue.shift();
1657
+ }
1658
+ else if (envelope.rcptQueue.length) {
1659
+ const nextRecipient = envelope.rcptQueue.shift();
1869
1660
  this._recipientQueue.push(nextRecipient);
1870
1661
  this._responseActions.push(str => {
1871
1662
  this._actionRCPT(str, callback);
@@ -1873,40 +1664,39 @@ class SMTPConnection extends EventEmitter {
1873
1664
  this._sendCommand('RCPT TO:<' + nextRecipient + '>' + this._getDsnRcptToArgs());
1874
1665
  }
1875
1666
  }
1876
-
1877
1667
  /**
1878
1668
  * Handle response for a DATA command
1879
1669
  *
1880
- * @param {String} str Message from the server
1670
+ * @param str Message from the server
1671
+ * @param callback Callback to run once the envelope is processed
1672
+ * @internal
1881
1673
  */
1882
1674
  _actionDATA(str, callback) {
1675
+ const envelope = this._envelope;
1883
1676
  // response should be 354 but according to this issue https://github.com/eleith/emailjs/issues/24
1884
1677
  // some servers might use 250 instead, so lets check for 2 or 3 as the first digit
1885
1678
  if (!/^[23]/.test(str)) {
1886
1679
  return callback(this._formatError('Data command failed', 'EENVELOPE', str, 'DATA'));
1887
1680
  }
1888
-
1889
1681
  const response = {
1890
- accepted: this._envelope.accepted,
1891
- rejected: this._envelope.rejected
1682
+ accepted: envelope.accepted,
1683
+ rejected: envelope.rejected
1892
1684
  };
1893
-
1894
1685
  if (this._ehloLines && this._ehloLines.length) {
1895
1686
  response.ehlo = this._ehloLines;
1896
1687
  }
1897
-
1898
- if (this._envelope.rejectedErrors.length) {
1899
- response.rejectedErrors = this._envelope.rejectedErrors;
1688
+ if (envelope.rejectedErrors.length) {
1689
+ response.rejectedErrors = envelope.rejectedErrors;
1900
1690
  }
1901
-
1902
1691
  callback(null, response);
1903
1692
  }
1904
-
1905
1693
  /**
1906
1694
  * Handle response for a DATA stream when using SMTP
1907
1695
  * We expect a single response that defines if the sending succeeded or failed
1908
1696
  *
1909
- * @param {String} str Message from the server
1697
+ * @param str Message from the server
1698
+ * @param callback Callback to run with the final send result
1699
+ * @internal
1910
1700
  */
1911
1701
  _actionSMTPStream(str, callback) {
1912
1702
  if (Number(str.charAt(0)) !== 2) {
@@ -1914,27 +1704,29 @@ class SMTPConnection extends EventEmitter {
1914
1704
  }
1915
1705
  return callback(null, str);
1916
1706
  }
1917
-
1918
1707
  /**
1919
1708
  * Handle response for a DATA stream
1920
1709
  * We expect a separate response for every recipient. All recipients can either
1921
1710
  * succeed or fail separately
1922
1711
  *
1923
- * @param {String} recipient The recipient this response applies to
1924
- * @param {Boolean} final Is this the final recipient?
1925
- * @param {String} str Message from the server
1712
+ * @param recipient The recipient this response applies to
1713
+ * @param final Is this the final recipient?
1714
+ * @param str Message from the server
1715
+ * @param callback Callback to run with the final send result
1716
+ * @internal
1926
1717
  */
1927
1718
  _actionLMTPStream(recipient, final, str, callback) {
1719
+ const envelope = this._envelope;
1928
1720
  let err;
1929
1721
  if (Number(str.charAt(0)) !== 2) {
1930
1722
  // Message failed
1931
1723
  err = this._formatError('Message failed for recipient ' + recipient, 'EMESSAGE', str, 'DATA');
1932
1724
  err.recipient = recipient;
1933
- this._envelope.rejected.push(recipient);
1934
- this._envelope.rejectedErrors.push(err);
1935
- for (let i = 0, len = this._envelope.accepted.length; i < len; i++) {
1936
- if (this._envelope.accepted[i] === recipient) {
1937
- this._envelope.accepted.splice(i, 1);
1725
+ envelope.rejected.push(recipient);
1726
+ envelope.rejectedErrors.push(err);
1727
+ for (let i = 0, len = envelope.accepted.length; i < len; i++) {
1728
+ if (envelope.accepted[i] === recipient) {
1729
+ envelope.accepted.splice(i, 1);
1938
1730
  }
1939
1731
  }
1940
1732
  }
@@ -1942,76 +1734,64 @@ class SMTPConnection extends EventEmitter {
1942
1734
  return callback(null, str);
1943
1735
  }
1944
1736
  }
1945
-
1737
+ /** @internal */
1946
1738
  _handleXOauth2Token(isRetry, callback) {
1947
1739
  this._auth.oauth2.getToken(isRetry, (err, accessToken) => {
1948
1740
  if (err) {
1949
- this.logger.info(
1950
- {
1951
- tnx: 'smtp',
1952
- username: this._auth.user,
1953
- action: 'authfail',
1954
- method: this._authMethod
1955
- },
1956
- 'User %s failed to authenticate',
1957
- JSON.stringify(this._auth.user)
1958
- );
1741
+ this.logger.info({
1742
+ tnx: 'smtp',
1743
+ username: this._auth.user,
1744
+ action: 'authfail',
1745
+ method: this._authMethod
1746
+ }, 'User %s failed to authenticate', JSON.stringify(this._auth.user));
1959
1747
  return callback(this._formatError(err, 'EAUTH', false, 'AUTH XOAUTH2'));
1960
1748
  }
1961
1749
  this._responseActions.push(str => {
1962
1750
  this._actionAUTHComplete(str, isRetry, callback);
1963
1751
  });
1964
- this._sendCommand(
1965
- 'AUTH XOAUTH2 ' + this._auth.oauth2.buildXOAuth2Token(accessToken),
1966
- // Hidden for logs
1967
- 'AUTH XOAUTH2 ' + this._auth.oauth2.buildXOAuth2Token('/* secret */')
1968
- );
1752
+ this._sendCommand('AUTH XOAUTH2 ' + this._auth.oauth2.buildXOAuth2Token(accessToken),
1753
+ // Hidden for logs
1754
+ 'AUTH XOAUTH2 ' + this._auth.oauth2.buildXOAuth2Token('/* secret */'));
1969
1755
  });
1970
1756
  }
1971
-
1972
1757
  /**
1973
1758
  *
1974
- * @param {string} command
1975
- * @private
1759
+ * @param command
1760
+ * @internal
1976
1761
  */
1977
1762
  _isDestroyedMessage(command) {
1978
1763
  if (this._destroyed) {
1979
1764
  return 'Cannot ' + command + ' - smtp connection is already destroyed.';
1980
1765
  }
1981
-
1982
1766
  if (this._socket) {
1983
1767
  if (this._socket.destroyed) {
1984
1768
  return 'Cannot ' + command + ' - smtp connection socket is already destroyed.';
1985
1769
  }
1986
-
1987
1770
  if (!this._socket.writable) {
1988
1771
  return 'Cannot ' + command + ' - smtp connection socket is already half-closed.';
1989
1772
  }
1990
1773
  }
1991
1774
  }
1992
-
1775
+ /** @internal */
1993
1776
  _getHostname() {
1994
1777
  // defaul hostname is machine hostname or [IP]
1995
1778
  let defaultHostname;
1996
1779
  try {
1997
1780
  defaultHostname = os.hostname() || '';
1998
- } catch (_err) {
1781
+ }
1782
+ catch (_err) {
1999
1783
  // fails on windows 7
2000
1784
  defaultHostname = 'localhost';
2001
1785
  }
2002
-
2003
1786
  // ignore if not FQDN
2004
1787
  if (!defaultHostname || defaultHostname.indexOf('.') < 0) {
2005
1788
  defaultHostname = '[127.0.0.1]';
2006
1789
  }
2007
-
2008
1790
  // IP should be enclosed in []
2009
1791
  if (defaultHostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
2010
1792
  defaultHostname = '[' + defaultHostname + ']';
2011
1793
  }
2012
-
2013
1794
  return defaultHostname;
2014
1795
  }
2015
1796
  }
2016
-
2017
- module.exports = SMTPConnection;
1797
+ export default SMTPConnection;