@nocobase/plugin-workflow-mailer 2.4.0-alpha.1 → 2.4.0-alpha.11

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 (187) hide show
  1. package/dist/externalVersion.js +6 -6
  2. package/dist/node_modules/joi/dist/joi-browser.min.js +1 -1
  3. package/dist/node_modules/joi/lib/index.js +1 -1
  4. package/dist/node_modules/joi/lib/messages.js +35 -6
  5. package/dist/node_modules/joi/lib/types/keys.js +18 -1
  6. package/dist/node_modules/joi/lib/types/link.js +18 -2
  7. package/dist/node_modules/joi/lib/types/string.js +2 -2
  8. package/dist/node_modules/joi/package.json +1 -1
  9. package/dist/node_modules/nodemailer/dist/cjs/addressparser/index.d.ts +59 -0
  10. package/dist/node_modules/nodemailer/dist/cjs/addressparser/index.js +790 -0
  11. package/dist/node_modules/nodemailer/dist/cjs/base64/index.d.ts +36 -0
  12. package/dist/node_modules/nodemailer/dist/cjs/base64/index.js +119 -0
  13. package/dist/node_modules/nodemailer/dist/cjs/dkim/index.d.ts +52 -0
  14. package/dist/node_modules/nodemailer/dist/cjs/dkim/index.js +228 -0
  15. package/dist/node_modules/nodemailer/dist/cjs/dkim/message-parser.d.ts +48 -0
  16. package/dist/node_modules/nodemailer/dist/cjs/dkim/message-parser.js +180 -0
  17. package/dist/node_modules/nodemailer/dist/cjs/dkim/relaxed-body.d.ts +36 -0
  18. package/dist/node_modules/nodemailer/dist/cjs/dkim/relaxed-body.js +148 -0
  19. package/dist/node_modules/nodemailer/dist/cjs/dkim/sign.d.ts +52 -0
  20. package/dist/node_modules/nodemailer/dist/cjs/dkim/sign.js +165 -0
  21. package/dist/node_modules/nodemailer/dist/cjs/errors.d.ts +96 -0
  22. package/dist/node_modules/nodemailer/dist/cjs/errors.js +70 -0
  23. package/dist/node_modules/nodemailer/dist/cjs/fetch/cookies.d.ts +107 -0
  24. package/dist/node_modules/nodemailer/dist/cjs/fetch/cookies.js +282 -0
  25. package/dist/node_modules/nodemailer/dist/cjs/fetch/index.d.ts +65 -0
  26. package/dist/node_modules/nodemailer/dist/cjs/fetch/index.js +384 -0
  27. package/dist/node_modules/nodemailer/dist/cjs/json-transport/index.d.ts +64 -0
  28. package/dist/node_modules/nodemailer/dist/cjs/json-transport/index.js +99 -0
  29. package/dist/node_modules/nodemailer/dist/cjs/mail-composer/index.d.ts +174 -0
  30. package/dist/node_modules/nodemailer/dist/cjs/mail-composer/index.js +613 -0
  31. package/dist/node_modules/nodemailer/dist/cjs/mailer/index.d.ts +237 -0
  32. package/dist/node_modules/nodemailer/dist/cjs/mailer/index.js +415 -0
  33. package/dist/node_modules/nodemailer/dist/cjs/mailer/mail-message.d.ts +97 -0
  34. package/dist/node_modules/nodemailer/dist/cjs/mailer/mail-message.js +342 -0
  35. package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/index.d.ts +161 -0
  36. package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/index.js +787 -0
  37. package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/mime-types.d.ts +2 -0
  38. package/dist/node_modules/nodemailer/dist/cjs/mime-funcs/mime-types.js +2103 -0
  39. package/dist/node_modules/nodemailer/dist/cjs/mime-node/index.d.ts +333 -0
  40. package/dist/node_modules/nodemailer/dist/cjs/mime-node/index.js +1493 -0
  41. package/dist/node_modules/nodemailer/dist/cjs/mime-node/last-newline.d.ts +5 -0
  42. package/dist/node_modules/nodemailer/dist/cjs/mime-node/last-newline.js +32 -0
  43. package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-unix.d.ts +9 -0
  44. package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-unix.js +40 -0
  45. package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-windows.d.ts +10 -0
  46. package/dist/node_modules/nodemailer/dist/cjs/mime-node/le-windows.js +47 -0
  47. package/dist/node_modules/nodemailer/dist/cjs/nodemailer.d.ts +124 -0
  48. package/dist/node_modules/nodemailer/dist/cjs/nodemailer.js +1 -0
  49. package/dist/node_modules/nodemailer/dist/cjs/package-info.d.ts +3 -0
  50. package/dist/node_modules/nodemailer/dist/cjs/package-info.js +7 -0
  51. package/dist/node_modules/nodemailer/dist/cjs/package.json +3 -0
  52. package/dist/node_modules/nodemailer/dist/cjs/punycode/index.d.ts +73 -0
  53. package/dist/node_modules/nodemailer/{lib → dist/cjs}/punycode/index.js +63 -112
  54. package/dist/node_modules/nodemailer/dist/cjs/qp/index.d.ts +32 -0
  55. package/dist/node_modules/nodemailer/dist/cjs/qp/index.js +207 -0
  56. package/dist/node_modules/nodemailer/dist/cjs/sendmail-transport/index.d.ts +69 -0
  57. package/dist/node_modules/nodemailer/dist/cjs/sendmail-transport/index.js +210 -0
  58. package/dist/node_modules/nodemailer/dist/cjs/ses-transport/index.d.ts +112 -0
  59. package/dist/node_modules/nodemailer/dist/cjs/ses-transport/index.js +252 -0
  60. package/dist/node_modules/nodemailer/dist/cjs/shared/index.d.ts +260 -0
  61. package/dist/node_modules/nodemailer/dist/cjs/shared/index.js +680 -0
  62. package/dist/node_modules/nodemailer/dist/cjs/shared/objects.d.ts +23 -0
  63. package/dist/node_modules/nodemailer/dist/cjs/shared/objects.js +43 -0
  64. package/dist/node_modules/nodemailer/dist/cjs/shared/url.d.ts +21 -0
  65. package/dist/node_modules/nodemailer/dist/cjs/shared/url.js +255 -0
  66. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/data-stream.d.ts +15 -0
  67. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/data-stream.js +108 -0
  68. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.d.ts +47 -0
  69. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.js +189 -0
  70. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/index.d.ts +416 -0
  71. package/dist/node_modules/nodemailer/dist/cjs/smtp-connection/index.js +1837 -0
  72. package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/index.d.ts +133 -0
  73. package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/index.js +569 -0
  74. package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/pool-resource.d.ts +62 -0
  75. package/dist/node_modules/nodemailer/dist/cjs/smtp-pool/pool-resource.js +257 -0
  76. package/dist/node_modules/nodemailer/dist/cjs/smtp-transport/index.d.ts +156 -0
  77. package/dist/node_modules/nodemailer/dist/cjs/smtp-transport/index.js +371 -0
  78. package/dist/node_modules/nodemailer/dist/cjs/stream-transport/index.d.ts +65 -0
  79. package/dist/node_modules/nodemailer/dist/cjs/stream-transport/index.js +146 -0
  80. package/dist/node_modules/nodemailer/dist/cjs/well-known/index.d.ts +35 -0
  81. package/dist/node_modules/nodemailer/dist/cjs/well-known/index.js +41 -0
  82. package/dist/node_modules/nodemailer/dist/cjs/well-known/services.d.ts +2 -0
  83. package/dist/node_modules/nodemailer/dist/cjs/well-known/services.js +674 -0
  84. package/dist/node_modules/nodemailer/dist/cjs/xoauth2/index.d.ts +201 -0
  85. package/dist/node_modules/nodemailer/dist/cjs/xoauth2/index.js +426 -0
  86. package/dist/node_modules/nodemailer/dist/esm/addressparser/index.d.ts +59 -0
  87. package/dist/node_modules/nodemailer/dist/esm/addressparser/index.js +786 -0
  88. package/dist/node_modules/nodemailer/dist/esm/base64/index.d.ts +36 -0
  89. package/dist/node_modules/nodemailer/{lib → dist/esm}/base64/index.js +39 -68
  90. package/dist/node_modules/nodemailer/dist/esm/dkim/index.d.ts +52 -0
  91. package/dist/node_modules/nodemailer/{lib → dist/esm}/dkim/index.js +62 -92
  92. package/dist/node_modules/nodemailer/dist/esm/dkim/message-parser.d.ts +48 -0
  93. package/dist/node_modules/nodemailer/dist/esm/dkim/message-parser.js +176 -0
  94. package/dist/node_modules/nodemailer/dist/esm/dkim/relaxed-body.d.ts +36 -0
  95. package/dist/node_modules/nodemailer/dist/esm/dkim/relaxed-body.js +141 -0
  96. package/dist/node_modules/nodemailer/dist/esm/dkim/sign.d.ts +52 -0
  97. package/dist/node_modules/nodemailer/dist/esm/dkim/sign.js +125 -0
  98. package/dist/node_modules/nodemailer/dist/esm/errors.d.ts +96 -0
  99. package/dist/node_modules/nodemailer/dist/esm/errors.js +67 -0
  100. package/dist/node_modules/nodemailer/dist/esm/fetch/cookies.d.ts +107 -0
  101. package/dist/node_modules/nodemailer/dist/esm/fetch/cookies.js +242 -0
  102. package/dist/node_modules/nodemailer/dist/esm/fetch/index.d.ts +65 -0
  103. package/dist/node_modules/nodemailer/dist/esm/fetch/index.js +344 -0
  104. package/dist/node_modules/nodemailer/dist/esm/json-transport/index.d.ts +64 -0
  105. package/dist/node_modules/nodemailer/dist/esm/json-transport/index.js +62 -0
  106. package/dist/node_modules/nodemailer/dist/esm/mail-composer/index.d.ts +174 -0
  107. package/dist/node_modules/nodemailer/{lib → dist/esm}/mail-composer/index.js +283 -275
  108. package/dist/node_modules/nodemailer/dist/esm/mailer/index.d.ts +237 -0
  109. package/dist/node_modules/nodemailer/{lib → dist/esm}/mailer/index.js +151 -205
  110. package/dist/node_modules/nodemailer/dist/esm/mailer/mail-message.d.ts +97 -0
  111. package/dist/node_modules/nodemailer/{lib → dist/esm}/mailer/mail-message.js +111 -125
  112. package/dist/node_modules/nodemailer/dist/esm/mime-funcs/index.d.ts +161 -0
  113. package/dist/node_modules/nodemailer/dist/esm/mime-funcs/index.js +738 -0
  114. package/dist/node_modules/nodemailer/dist/esm/mime-funcs/mime-types.d.ts +2 -0
  115. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-funcs/mime-types.js +43 -49
  116. package/dist/node_modules/nodemailer/dist/esm/mime-node/index.d.ts +333 -0
  117. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/index.js +576 -437
  118. package/dist/node_modules/nodemailer/dist/esm/mime-node/last-newline.d.ts +5 -0
  119. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/last-newline.js +4 -10
  120. package/dist/node_modules/nodemailer/dist/esm/mime-node/le-unix.d.ts +9 -0
  121. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/le-unix.js +7 -15
  122. package/dist/node_modules/nodemailer/dist/esm/mime-node/le-windows.d.ts +10 -0
  123. package/dist/node_modules/nodemailer/{lib → dist/esm}/mime-node/le-windows.js +6 -16
  124. package/dist/node_modules/nodemailer/dist/esm/nodemailer.d.ts +124 -0
  125. package/dist/node_modules/nodemailer/dist/esm/nodemailer.js +166 -0
  126. package/dist/node_modules/nodemailer/dist/esm/package-info.d.ts +3 -0
  127. package/dist/node_modules/nodemailer/dist/esm/package-info.js +4 -0
  128. package/dist/node_modules/nodemailer/dist/esm/package.json +3 -0
  129. package/dist/node_modules/nodemailer/dist/esm/punycode/index.d.ts +73 -0
  130. package/dist/node_modules/nodemailer/dist/esm/punycode/index.js +405 -0
  131. package/dist/node_modules/nodemailer/dist/esm/qp/index.d.ts +32 -0
  132. package/dist/node_modules/nodemailer/{lib → dist/esm}/qp/index.js +59 -77
  133. package/dist/node_modules/nodemailer/dist/esm/sendmail-transport/index.d.ts +69 -0
  134. package/dist/node_modules/nodemailer/dist/esm/sendmail-transport/index.js +170 -0
  135. package/dist/node_modules/nodemailer/dist/esm/ses-transport/index.d.ts +112 -0
  136. package/dist/node_modules/nodemailer/dist/esm/ses-transport/index.js +212 -0
  137. package/dist/node_modules/nodemailer/dist/esm/shared/index.d.ts +260 -0
  138. package/dist/node_modules/nodemailer/dist/esm/shared/index.js +631 -0
  139. package/dist/node_modules/nodemailer/dist/esm/shared/objects.d.ts +23 -0
  140. package/dist/node_modules/nodemailer/dist/esm/shared/objects.js +38 -0
  141. package/dist/node_modules/nodemailer/dist/esm/shared/url.d.ts +21 -0
  142. package/dist/node_modules/nodemailer/dist/esm/shared/url.js +214 -0
  143. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/data-stream.d.ts +15 -0
  144. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/data-stream.js +103 -0
  145. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.d.ts +47 -0
  146. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.js +149 -0
  147. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/index.d.ts +416 -0
  148. package/dist/node_modules/nodemailer/dist/esm/smtp-connection/index.js +1797 -0
  149. package/dist/node_modules/nodemailer/dist/esm/smtp-pool/index.d.ts +133 -0
  150. package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-pool/index.js +195 -314
  151. package/dist/node_modules/nodemailer/dist/esm/smtp-pool/pool-resource.d.ts +62 -0
  152. package/dist/node_modules/nodemailer/{lib → dist/esm}/smtp-pool/pool-resource.js +70 -107
  153. package/dist/node_modules/nodemailer/dist/esm/smtp-transport/index.d.ts +156 -0
  154. package/dist/node_modules/nodemailer/dist/esm/smtp-transport/index.js +331 -0
  155. package/dist/node_modules/nodemailer/dist/esm/stream-transport/index.d.ts +65 -0
  156. package/dist/node_modules/nodemailer/dist/esm/stream-transport/index.js +106 -0
  157. package/dist/node_modules/nodemailer/dist/esm/well-known/index.d.ts +35 -0
  158. package/dist/node_modules/nodemailer/{lib → dist/esm}/well-known/index.js +12 -23
  159. package/dist/node_modules/nodemailer/dist/esm/well-known/services.d.ts +2 -0
  160. package/dist/node_modules/nodemailer/dist/esm/well-known/services.js +671 -0
  161. package/dist/node_modules/nodemailer/dist/esm/xoauth2/index.d.ts +201 -0
  162. package/dist/node_modules/nodemailer/dist/esm/xoauth2/index.js +386 -0
  163. package/dist/node_modules/nodemailer/dist/well-known/services.json +640 -0
  164. package/dist/node_modules/nodemailer/package.json +1 -1
  165. package/package.json +4 -4
  166. package/dist/node_modules/nodemailer/.gitattributes +0 -6
  167. package/dist/node_modules/nodemailer/.ncurc.js +0 -7
  168. package/dist/node_modules/nodemailer/.prettierrc.js +0 -8
  169. package/dist/node_modules/nodemailer/lib/addressparser/index.js +0 -313
  170. package/dist/node_modules/nodemailer/lib/dkim/message-parser.js +0 -155
  171. package/dist/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
  172. package/dist/node_modules/nodemailer/lib/dkim/sign.js +0 -117
  173. package/dist/node_modules/nodemailer/lib/fetch/cookies.js +0 -281
  174. package/dist/node_modules/nodemailer/lib/fetch/index.js +0 -274
  175. package/dist/node_modules/nodemailer/lib/json-transport/index.js +0 -82
  176. package/dist/node_modules/nodemailer/lib/mime-funcs/index.js +0 -625
  177. package/dist/node_modules/nodemailer/lib/nodemailer.js +0 -1
  178. package/dist/node_modules/nodemailer/lib/sendmail-transport/index.js +0 -210
  179. package/dist/node_modules/nodemailer/lib/ses-transport/index.js +0 -349
  180. package/dist/node_modules/nodemailer/lib/shared/index.js +0 -688
  181. package/dist/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -108
  182. package/dist/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +0 -143
  183. package/dist/node_modules/nodemailer/lib/smtp-connection/index.js +0 -1825
  184. package/dist/node_modules/nodemailer/lib/smtp-transport/index.js +0 -416
  185. package/dist/node_modules/nodemailer/lib/stream-transport/index.js +0 -135
  186. package/dist/node_modules/nodemailer/lib/well-known/services.json +0 -343
  187. package/dist/node_modules/nodemailer/lib/xoauth2/index.js +0 -376
@@ -0,0 +1,1797 @@
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';
10
+ // default timeout values in ms
11
+ const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection to be established
12
+ const SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity before disconnecting the client
13
+ const GREETING_TIMEOUT = 30 * 1000; // how much to wait after connection is established but SMTP greeting is not receieved
14
+ const DNS_TIMEOUT = 30 * 1000; // how much to wait for resolveHostname
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;
19
+ /**
20
+ * Re-interpret a server response stored in fake 8-bit byte-container form
21
+ * (the result of chunk.toString('binary') in _onData) as UTF-8.
22
+ *
23
+ * Server reply text has no formally defined charset (RFC 5321 §4.2.1), but
24
+ * modern MTAs commonly use UTF-8. The byte-container plumbing in _onData is
25
+ * required to reassemble multi-byte sequences split across socket chunks;
26
+ * this helper performs the actual decode at the line boundary, falling back
27
+ * to the byte-container form when the bytes are not valid UTF-8 so that
28
+ * legacy 8-bit replies are still recoverable byte-for-byte.
29
+ */
30
+ function decodeServerResponse(str) {
31
+ if (!str) {
32
+ return str;
33
+ }
34
+ const utf8 = Buffer.from(str, 'binary').toString('utf8');
35
+ // The input is a byte container (each char is in U+0000..U+00FF) so it can never
36
+ // already contain U+FFFD; any \uFFFD in the result was inserted by Node's UTF-8
37
+ // decoder for invalid bytes, which means we should return the original bytes intact.
38
+ return utf8.includes('\uFFFD') ? str : utf8;
39
+ }
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
+ }
61
+ /**
62
+ * Generates a SMTP connection object
63
+ *
64
+ * Optional options object takes the following possible properties:
65
+ *
66
+ * * **port** - is the port to connect to (defaults to 587 or 465)
67
+ * * **host** - is the hostname or IP address to connect to (defaults to 'localhost')
68
+ * * **secure** - use SSL
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
71
+ * * **name** - the name of the client server
72
+ * * **localAddress** - outbound address to bind to (see: http://nodejs.org/api/net.html#net_net_connect_options_connectionlistener)
73
+ * * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 30 seconds)
74
+ * * **connectionTimeout** - how many milliseconds to wait for the connection to establish (defaults to 2 minutes)
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)
77
+ * * **dnsTimeout** - Time to wait in ms for the DNS requests to be resolved (defaults to 30 seconds)
78
+ * * **lmtp** - if true, uses LMTP instead of SMTP protocol
79
+ * * **logger** - bunyan compatible logger interface
80
+ * * **debug** - if true pass SMTP traffic to the logger
81
+ * * **tls** - options for createCredentials
82
+ * * **socket** - existing socket to use instead of creating a new one (see: http://nodejs.org/api/net.html#net_class_net_socket)
83
+ * * **secured** - boolean indicates that the provided socket has already been upgraded to tls
84
+ *
85
+ * @constructor
86
+ * @namespace SMTP Client module
87
+ * @param [options] Option properties
88
+ */
89
+ class SMTPConnection extends EventEmitter {
90
+ constructor(options) {
91
+ super(options);
92
+ this.id = crypto.randomBytes(8).toString('base64').replace(/\W/g, '');
93
+ this.stage = 'init';
94
+ this.options = options || {};
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
+ }
100
+ this.secureConnection = !!this.options.secure;
101
+ this.alreadySecured = !!this.options.secured;
102
+ this.port = Number(this.options.port) || (this.secureConnection ? 465 : 587);
103
+ this.host = this.options.host || 'localhost';
104
+ this.servername = this.options.servername ? this.options.servername : !net.isIP(this.host) ? this.host : false;
105
+ this.allowInternalNetworkInterfaces = this.options.allowInternalNetworkInterfaces || false;
106
+ if (typeof this.options.secure === 'undefined' && this.port === 465) {
107
+ // if secure option is not set but port is 465, then default to secure
108
+ this.secureConnection = true;
109
+ }
110
+ this.name = (this.options.name || this._getHostname()).toString().replace(/[\r\n]+/g, '');
111
+ this.logger = shared.getLogger(this.options, {
112
+ component: this.options.component || 'smtp-connection',
113
+ sid: this.id
114
+ });
115
+ this.customAuth = new Map();
116
+ for (const key of Object.keys(this.options.customAuth || {})) {
117
+ const mapKey = (key || '').toString().trim().toUpperCase();
118
+ if (mapKey) {
119
+ this.customAuth.set(mapKey, this.options.customAuth[key]);
120
+ }
121
+ }
122
+ this.version = packageData.version;
123
+ this.authenticated = false;
124
+ this.destroyed = false;
125
+ this.secure = !!this.secureConnection;
126
+ this._remainder = '';
127
+ this._responseQueue = [];
128
+ this._responsePartial = false;
129
+ this.lastServerResponse = false;
130
+ this._socket = false;
131
+ this._supportedAuth = [];
132
+ this.allowsAuth = false;
133
+ this._envelope = false;
134
+ this._supportedExtensions = [];
135
+ this._maxAllowedSize = 0;
136
+ this._responseActions = [];
137
+ this._recipientQueue = [];
138
+ this._greetingTimeout = false;
139
+ this._connectionTimeout = false;
140
+ this._destroyed = false;
141
+ this._closing = false;
142
+ this._currentDataStream = false;
143
+ this._pendingSend = false;
144
+ this._connectCallback = false;
145
+ this._onSocketData = chunk => this._onData(chunk);
146
+ this._onSocketError = error => this._onError(error, 'ESOCKET', false, 'CONN');
147
+ this._onSocketClose = () => this._onClose();
148
+ this._onSocketEnd = () => this._onEnd();
149
+ this._onSocketTimeout = () => this._onTimeout();
150
+ this._onConnectionSocketError = err => this._onConnectionError(err, 'ESOCKET');
151
+ this._connectionAttemptId = 0;
152
+ }
153
+ /**
154
+ * Creates a connection to a SMTP server and sets up connection
155
+ * listener
156
+ */
157
+ connect(connectCallback) {
158
+ if (typeof connectCallback === 'function') {
159
+ this._connectCallback = connectCallback;
160
+ this.once('connect', () => {
161
+ this._connectCallback = false;
162
+ this.logger.debug({
163
+ tnx: 'smtp'
164
+ }, 'SMTP handshake finished');
165
+ connectCallback();
166
+ });
167
+ const isDestroyedMessage = this._isDestroyedMessage('connect');
168
+ if (isDestroyedMessage) {
169
+ return connectCallback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'CONN'));
170
+ }
171
+ }
172
+ let opts = {
173
+ port: this.port,
174
+ host: this.host,
175
+ allowInternalNetworkInterfaces: this.allowInternalNetworkInterfaces,
176
+ timeout: this.options.dnsTimeout || DNS_TIMEOUT
177
+ };
178
+ if (this.options.localAddress) {
179
+ opts.localAddress = this.options.localAddress;
180
+ }
181
+ if (this.options.connection) {
182
+ // connection is already opened
183
+ this._socket = this.options.connection;
184
+ this._setupConnectionHandlers();
185
+ if (this.secureConnection && !this.alreadySecured) {
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 {
195
+ setImmediate(() => this._onConnect());
196
+ }
197
+ return;
198
+ }
199
+ else if (this.options.socket) {
200
+ // socket object is set up but not yet connected
201
+ this._socket = this.options.socket;
202
+ return this._resolveAndConnect(opts, _resolved => {
203
+ try {
204
+ this._socket.connect(this.port, this.host, () => {
205
+ this._socket.setKeepAlive(true);
206
+ // a `secure` connection over a caller-provided socket must still
207
+ // perform the TLS handshake, otherwise AUTH and the message body
208
+ // would be sent in cleartext despite the caller requesting TLS
209
+ if (this.secureConnection && !this.alreadySecured) {
210
+ return this._upgradeConnection(err => {
211
+ if (err) {
212
+ this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'CONN');
213
+ return;
214
+ }
215
+ this._onConnect();
216
+ });
217
+ }
218
+ this._onConnect();
219
+ });
220
+ this._setupConnectionHandlers();
221
+ }
222
+ catch (E) {
223
+ setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
224
+ return;
225
+ }
226
+ });
227
+ }
228
+ else {
229
+ if (this.secureConnection) {
230
+ Object.assign(opts, this.options.tls || {});
231
+ // ensure servername for SNI
232
+ if (this.servername && !opts.servername) {
233
+ opts.servername = this.servername;
234
+ }
235
+ }
236
+ return this._resolveAndConnect(opts, resolved => {
237
+ // Store fallback addresses for retry on connection failure
238
+ this._fallbackAddresses = (resolved._addresses || []).filter(addr => addr !== opts.host);
239
+ this._connectOpts = Object.assign({}, opts);
240
+ this._connectToHost(opts, this.secureConnection);
241
+ });
242
+ }
243
+ }
244
+ /**
245
+ * Resolves the hostname and applies resolved values to opts,
246
+ * then calls the provided callback with the resolved data
247
+ *
248
+ * @param opts Connection options (modified in place)
249
+ * @param callback Called with resolved data on success
250
+ * @internal
251
+ */
252
+ _resolveAndConnect(opts, callback) {
253
+ return shared.resolveHostname(opts, (err, resolved) => {
254
+ if (err) {
255
+ return setImmediate(() => this._onError(err, 'EDNS', false, 'CONN'));
256
+ }
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');
263
+ for (const key of Object.keys(resolved)) {
264
+ if (key.charAt(0) !== '_' && resolved[key]) {
265
+ opts[key] = resolved[key];
266
+ }
267
+ }
268
+ callback(resolved);
269
+ });
270
+ }
271
+ /**
272
+ * Attempts to connect to the specified host address
273
+ *
274
+ * @param opts Connection options
275
+ * @param secure Whether to use TLS
276
+ * @internal
277
+ */
278
+ _connectToHost(opts, secure) {
279
+ // If the client was closed while DNS resolution was in flight, do not open
280
+ // a socket here: close() ran with this._socket still unset and so had
281
+ // nothing to tear down, and _onConnect's remedial close() is a no-op once
282
+ // _closing is set, the freshly connected socket would leak.
283
+ if (this._destroyed || this._closing) {
284
+ return;
285
+ }
286
+ this._connectionAttemptId++;
287
+ const currentAttemptId = this._connectionAttemptId;
288
+ const connectFn = secure
289
+ ? tls.connect
290
+ : net.connect;
291
+ try {
292
+ this._socket = connectFn(opts, () => {
293
+ // Ignore callback if this is a stale connection attempt
294
+ if (this._connectionAttemptId !== currentAttemptId) {
295
+ return;
296
+ }
297
+ this._socket.setKeepAlive(true);
298
+ this._onConnect();
299
+ });
300
+ this._setupConnectionHandlers();
301
+ }
302
+ catch (E) {
303
+ setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
304
+ return;
305
+ }
306
+ }
307
+ /**
308
+ * Sets up connection timeout and error handlers
309
+ * @internal
310
+ */
311
+ _setupConnectionHandlers() {
312
+ this._connectionTimeout = setTimeout(() => {
313
+ this._onConnectionError('Connection timeout', 'ETIMEDOUT');
314
+ }, this.options.connectionTimeout || CONNECTION_TIMEOUT);
315
+ this._socket.on('error', this._onConnectionSocketError);
316
+ }
317
+ /**
318
+ * Handles connection errors with fallback to alternative addresses
319
+ *
320
+ * @param err Error object or message
321
+ * @param code Error code
322
+ * @internal
323
+ */
324
+ _onConnectionError(err, code) {
325
+ clearTimeout(this._connectionTimeout);
326
+ // Check if we have fallback addresses to try
327
+ const canFallback = this._fallbackAddresses && this._fallbackAddresses.length && this.stage === 'init' && !this._destroyed;
328
+ if (!canFallback) {
329
+ // No more fallback addresses, report the error
330
+ this._onError(err, code, false, 'CONN');
331
+ return;
332
+ }
333
+ const nextHost = this._fallbackAddresses.shift();
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);
340
+ // Clean up current socket
341
+ if (this._socket) {
342
+ try {
343
+ this._socket.removeListener('error', this._onConnectionSocketError);
344
+ // Absorb any late teardown error (e.g. a TLS fallback socket emitting
345
+ // after destroy), mirroring the guard used in close()
346
+ this._socket.on('error', TEARDOWN_NOOP);
347
+ this._socket.destroy();
348
+ }
349
+ catch (_E) {
350
+ // ignore
351
+ }
352
+ this._socket = null;
353
+ }
354
+ // Update host and retry
355
+ this._connectOpts.host = nextHost;
356
+ this._connectToHost(this._connectOpts, this.secureConnection);
357
+ }
358
+ /**
359
+ * Sends QUIT
360
+ */
361
+ quit() {
362
+ this._sendCommand('QUIT');
363
+ this._responseActions.push(this.close);
364
+ }
365
+ /**
366
+ * Closes the connection to the server
367
+ */
368
+ close() {
369
+ clearTimeout(this._connectionTimeout);
370
+ clearTimeout(this._greetingTimeout);
371
+ this._responseActions = [];
372
+ // allow to run this function only once
373
+ if (this._closing) {
374
+ return;
375
+ }
376
+ this._closing = true;
377
+ const closeMethod = this.stage === 'init' ? 'destroy' : 'end';
378
+ this.logger.debug({
379
+ tnx: 'smtp'
380
+ }, 'Closing connection to the server using "%s"', closeMethod);
381
+ const socket = (this._socket && this._socket.socket) || this._socket;
382
+ // Detach any in-flight DATA stream from the socket so the source stream
383
+ // can be garbage-collected once the socket is gone.
384
+ if (this._currentDataStream) {
385
+ try {
386
+ this._currentDataStream.unpipe(this._socket);
387
+ }
388
+ catch (_E) {
389
+ // ignore
390
+ }
391
+ this._currentDataStream = false;
392
+ }
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
+ }
403
+ if (socket && !socket.destroyed) {
404
+ try {
405
+ // Clear socket timeout to prevent timer leaks
406
+ socket.setTimeout(0);
407
+ // Remove all listeners to allow proper garbage collection
408
+ socket.removeListener('data', this._onSocketData);
409
+ socket.removeListener('timeout', this._onSocketTimeout);
410
+ socket.removeListener('close', this._onSocketClose);
411
+ socket.removeListener('end', this._onSocketEnd);
412
+ socket.removeListener('error', this._onSocketError);
413
+ socket.removeListener('error', this._onConnectionSocketError);
414
+ // Absorb errors that may fire during socket teardown (e.g. server
415
+ // sending cleartext after TLS shutdown triggers ERR_SSL_BAD_RECORD_TYPE)
416
+ socket.on('error', TEARDOWN_NOOP);
417
+ socket[closeMethod]();
418
+ }
419
+ catch (_E) {
420
+ // just ignore
421
+ }
422
+ }
423
+ this._destroy();
424
+ }
425
+ /**
426
+ * Authenticate user
427
+ */
428
+ login(authData, callback) {
429
+ const isDestroyedMessage = this._isDestroyedMessage('login');
430
+ if (isDestroyedMessage) {
431
+ return callback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API'));
432
+ }
433
+ this._auth = authData || {};
434
+ // Select SASL authentication method
435
+ this._authMethod = (this._auth.method || '').toString().trim().toUpperCase() || false;
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');
439
+ if (!this._authMethod && this._auth.oauth2 && !this._auth.credentials) {
440
+ this._authMethod = 'XOAUTH2';
441
+ }
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)) {
451
+ if ((this._auth.user && this._auth.pass) || this.customAuth.has(this._authMethod)) {
452
+ this._auth.credentials = {
453
+ user: this._auth.user,
454
+ pass: this._auth.pass,
455
+ options: this._auth.options
456
+ };
457
+ }
458
+ else {
459
+ return callback(this._formatError('Missing credentials for "' + this._authMethod + '"', 'EAUTH', false, 'API'));
460
+ }
461
+ }
462
+ if (this.customAuth.has(this._authMethod)) {
463
+ const handler = this.customAuth.get(this._authMethod);
464
+ let lastResponse;
465
+ let returned = false;
466
+ const resolve = () => {
467
+ if (returned) {
468
+ return;
469
+ }
470
+ returned = true;
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));
477
+ this.authenticated = true;
478
+ callback(null, true);
479
+ };
480
+ const reject = (err) => {
481
+ if (returned) {
482
+ return;
483
+ }
484
+ returned = true;
485
+ callback(this._formatError(err, 'EAUTH', lastResponse, 'AUTH ' + this._authMethod));
486
+ };
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
+ };
519
+ const handlerResponse = handler({
520
+ auth: this._auth,
521
+ method: this._authMethod,
522
+ extensions: [].concat(this._supportedExtensions),
523
+ authMethods: [].concat(this._supportedAuth),
524
+ maxAllowedSize: this._maxAllowedSize || false,
525
+ sendCommand: sendCommand,
526
+ resolve,
527
+ reject
528
+ });
529
+ if (handlerResponse && typeof handlerResponse.catch === 'function') {
530
+ // a promise was returned
531
+ handlerResponse.then(resolve).catch(reject);
532
+ }
533
+ return;
534
+ }
535
+ switch (this._authMethod) {
536
+ case 'XOAUTH2':
537
+ this._handleXOauth2Token(false, callback);
538
+ return;
539
+ case 'LOGIN':
540
+ this._responseActions.push(str => {
541
+ this._actionAUTH_LOGIN_USER(str, callback);
542
+ });
543
+ this._sendCommand('AUTH LOGIN');
544
+ return;
545
+ case 'PLAIN':
546
+ this._responseActions.push(str => {
547
+ this._actionAUTHComplete(str, callback);
548
+ });
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'));
564
+ return;
565
+ case 'CRAM-MD5':
566
+ this._responseActions.push(str => {
567
+ this._actionAUTH_CRAM_MD5(str, callback);
568
+ });
569
+ this._sendCommand('AUTH CRAM-MD5');
570
+ return;
571
+ }
572
+ return callback(this._formatError('Unknown authentication method "' + this._authMethod + '"', 'EAUTH', false, 'API'));
573
+ }
574
+ /**
575
+ * Sends a message
576
+ *
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
580
+ */
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
+ };
595
+ if (!message) {
596
+ return callback(this._formatError('Empty message', 'EMESSAGE', false, 'API'));
597
+ }
598
+ const isDestroyedMessage = this._isDestroyedMessage('send message');
599
+ if (isDestroyedMessage) {
600
+ return callback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API'));
601
+ }
602
+ // reject larger messages than allowed
603
+ if (this._maxAllowedSize && envelope.size > this._maxAllowedSize) {
604
+ setImmediate(() => {
605
+ callback(this._formatError('Message size larger than allowed ' + this._maxAllowedSize, 'EMESSAGE', false, 'MAIL FROM'));
606
+ });
607
+ return;
608
+ }
609
+ const pendingSend = {
610
+ callback,
611
+ stream: false,
612
+ onStreamError: err => callback(this._formatError(err, 'ESTREAM', false, 'API'))
613
+ };
614
+ if (typeof message.on === 'function') {
615
+ pendingSend.stream = message;
616
+ pendingSend.stream.on('error', pendingSend.onStreamError);
617
+ }
618
+ this._pendingSend = pendingSend;
619
+ const startTime = Date.now();
620
+ this._setEnvelope(envelope, (err, info) => {
621
+ if (err) {
622
+ // create passthrough stream to consume to prevent OOM
623
+ const stream = new PassThrough();
624
+ if (typeof message.pipe === 'function') {
625
+ message.pipe(stream);
626
+ }
627
+ else {
628
+ stream.write(message);
629
+ stream.end();
630
+ }
631
+ return callback(err);
632
+ }
633
+ const envelopeTime = Date.now();
634
+ const stream = this._createSendStream((err, str) => {
635
+ if (err) {
636
+ return callback(err);
637
+ }
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);
645
+ });
646
+ if (typeof message.pipe === 'function') {
647
+ message.pipe(stream);
648
+ }
649
+ else {
650
+ stream.write(message);
651
+ stream.end();
652
+ }
653
+ });
654
+ }
655
+ /**
656
+ * Resets connection state
657
+ *
658
+ * @param callback Callback to return once connection is reset
659
+ */
660
+ reset(callback) {
661
+ const isDestroyedMessage = this._isDestroyedMessage('reset');
662
+ if (isDestroyedMessage) {
663
+ return callback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API'));
664
+ }
665
+ this._sendCommand('RSET');
666
+ this._responseActions.push(str => {
667
+ if (str.charAt(0) !== '2') {
668
+ return callback(this._formatError('Could not reset session state. response=' + str, 'EPROTOCOL', str, 'RSET'));
669
+ }
670
+ this._envelope = false;
671
+ return callback(null, true);
672
+ });
673
+ }
674
+ /**
675
+ * Connection listener that is run when the connection to
676
+ * the server is opened
677
+ *
678
+ * @event
679
+ * @internal
680
+ */
681
+ _onConnect() {
682
+ const socket = this._socket;
683
+ clearTimeout(this._connectionTimeout);
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);
691
+ if (this._destroyed) {
692
+ // Connection was established after we already had canceled it
693
+ this.close();
694
+ return;
695
+ }
696
+ this.stage = 'connected';
697
+ // clear existing listeners for the socket
698
+ socket.removeListener('data', this._onSocketData);
699
+ socket.removeListener('timeout', this._onSocketTimeout);
700
+ socket.removeListener('close', this._onSocketClose);
701
+ socket.removeListener('end', this._onSocketEnd);
702
+ // Switch from connection-phase error handler to normal error handler
703
+ socket.removeListener('error', this._onConnectionSocketError);
704
+ // _upgradeConnection (options.connection + secure) may already have attached
705
+ // the normal handler; remove it first so we never end up with a duplicate
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);
713
+ this._greetingTimeout = setTimeout(() => {
714
+ // if still waiting for greeting, give up
715
+ if (this._socket && !this._destroyed && this._responseActions[0] === this._actionGreeting) {
716
+ this._onError('Greeting never received', 'ETIMEDOUT', false, 'CONN');
717
+ }
718
+ }, this.options.greetingTimeout || GREETING_TIMEOUT);
719
+ this._responseActions.push(this._actionGreeting);
720
+ // we have a 'data' listener set up so resume socket if it was paused
721
+ socket.resume();
722
+ }
723
+ /**
724
+ * 'data' listener for data coming from the server
725
+ *
726
+ * @event
727
+ * @param chunk Data chunk coming from the server
728
+ * @internal
729
+ */
730
+ _onData(chunk) {
731
+ if (this._destroyed || !chunk || !chunk.length) {
732
+ return;
733
+ }
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/);
748
+ this._remainder = lines.pop();
749
+ for (let i = 0, len = lines.length; i < len; i++) {
750
+ if (this._responsePartial) {
751
+ this._responseQueue[this._responseQueue.length - 1] += '\n' + lines[i];
752
+ }
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();
765
+ }
766
+ }
767
+ if (this._remainder.length > maxResponseSize) {
768
+ return this._onResponseTooLarge();
769
+ }
770
+ if (this._responsePartial) {
771
+ return;
772
+ }
773
+ this._processResponse();
774
+ }
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
+ }
786
+ /**
787
+ * 'error' listener for the socket
788
+ *
789
+ * @event
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
795
+ */
796
+ _onError(err, type, data, command) {
797
+ clearTimeout(this._connectionTimeout);
798
+ clearTimeout(this._greetingTimeout);
799
+ if (this._destroyed) {
800
+ // just ignore, already closed
801
+ // this might happen when a socket is canceled because of reached timeout
802
+ // but the socket timeout error itself receives only after
803
+ return;
804
+ }
805
+ err = this._formatError(err, type, data, command);
806
+ const transientCodes = ['ETIMEDOUT', 'ESOCKET', 'ECONNECTION'];
807
+ if (transientCodes.includes(err.code)) {
808
+ this.logger.warn(data, err.message);
809
+ }
810
+ else {
811
+ this.logger.error(data, err.message);
812
+ }
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;
816
+ this.emit('error', err);
817
+ this.close();
818
+ if (pendingSend) {
819
+ pendingSend.callback(err);
820
+ }
821
+ }
822
+ /** @internal */
823
+ _formatError(message, type, response, command) {
824
+ let err;
825
+ if (/Error\]$/i.test(Object.prototype.toString.call(message))) {
826
+ err = message;
827
+ }
828
+ else {
829
+ err = new Error(message);
830
+ }
831
+ if (type && type !== 'Error') {
832
+ err.code = type;
833
+ }
834
+ if (response) {
835
+ err.response = response;
836
+ err.message += ': ' + response;
837
+ }
838
+ const responseCode = (typeof response === 'string' && Number((response.match(/^\d+/) || [])[0])) || false;
839
+ if (responseCode) {
840
+ err.responseCode = responseCode;
841
+ }
842
+ if (command) {
843
+ err.command = command;
844
+ }
845
+ return err;
846
+ }
847
+ /**
848
+ * 'close' listener for the socket
849
+ *
850
+ * @event
851
+ * @internal
852
+ */
853
+ _onClose() {
854
+ let serverResponse = false;
855
+ if (this._remainder && this._remainder.trim()) {
856
+ this.lastServerResponse = serverResponse = decodeServerResponse(this._remainder.trim());
857
+ if (this.options.debug || this.options.transactionLog) {
858
+ this.logger.debug({
859
+ tnx: 'server'
860
+ }, serverResponse);
861
+ }
862
+ }
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;
869
+ if (this.upgrading && !this._destroyed) {
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');
885
+ }
886
+ this._destroy();
887
+ }
888
+ /**
889
+ * 'end' listener for the socket
890
+ *
891
+ * @event
892
+ * @internal
893
+ */
894
+ _onEnd() {
895
+ if (this._socket && !this._socket.destroyed) {
896
+ // Peer sent FIN, finish our half of the close gracefully rather
897
+ // than destroying. 'close' fires after the OS finalizes teardown.
898
+ this._socket.end();
899
+ }
900
+ }
901
+ /**
902
+ * 'timeout' listener for the socket
903
+ *
904
+ * @event
905
+ * @internal
906
+ */
907
+ _onTimeout() {
908
+ return this._onError(new Error('Timeout'), 'ETIMEDOUT', false, 'CONN');
909
+ }
910
+ /**
911
+ * Destroys the client, emits 'end'
912
+ * @internal
913
+ */
914
+ _destroy() {
915
+ if (this._destroyed) {
916
+ return;
917
+ }
918
+ this._destroyed = true;
919
+ // keep the documented public flag in sync with the private state
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;
927
+ this.emit('end');
928
+ }
929
+ /**
930
+ * Upgrades the connection to TLS
931
+ *
932
+ * @param callback Callback function to run when the connection
933
+ * has been secured
934
+ * @internal
935
+ */
936
+ _upgradeConnection(callback) {
937
+ // RFC 3207 section 6: the client MUST discard any knowledge obtained from
938
+ // the server that was not received over the TLS-protected session. Drop any
939
+ // buffered input received before the handshake so a man-in-the-middle cannot
940
+ // inject plaintext bytes after the "220" reply (e.g. a CRLF-free fragment that
941
+ // would otherwise be prepended to the first post-TLS response and parsed as
942
+ // part of the secured EHLO capabilities). STARTTLS response injection.
943
+ this._remainder = '';
944
+ this._responseQueue = [];
945
+ this._responsePartial = false;
946
+ // do not remove all listeners or it breaks node v0.10 as there's
947
+ // apparently a 'finish' event set that would be cleared as well
948
+ // we can safely keep 'error', 'end', 'close' etc. events
949
+ const socketPlain = this._socket;
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 || {});
956
+ // ensure servername for SNI
957
+ if (this.servername && !opts.servername) {
958
+ opts.servername = this.servername;
959
+ }
960
+ // Remove all listeners from the plain socket to allow proper garbage
961
+ // collection. Used on both the TLS-success path and the synchronous
962
+ // tls.connect() throw path; either way the plain socket is done.
963
+ const removePlainSocketListeners = () => {
964
+ socketPlain.removeListener('close', this._onSocketClose);
965
+ socketPlain.removeListener('end', this._onSocketEnd);
966
+ socketPlain.removeListener('error', this._onSocketError);
967
+ // the connection-phase handler is attached when upgrading a pre-opened
968
+ // options.connection socket; strip it so nothing lingers on the plain socket
969
+ socketPlain.removeListener('error', this._onConnectionSocketError);
970
+ };
971
+ this.upgrading = true;
972
+ // tls.connect is not an asynchronous function however it may still throw errors and requires to be wrapped with try/catch
973
+ try {
974
+ this._socket = tls.connect(opts, () => {
975
+ this.secure = true;
976
+ this.upgrading = false;
977
+ this._socket.on('data', this._onSocketData);
978
+ removePlainSocketListeners();
979
+ return callback(null, true);
980
+ });
981
+ }
982
+ catch (err) {
983
+ removePlainSocketListeners();
984
+ return callback(err);
985
+ }
986
+ this._socket.on('error', this._onSocketError);
987
+ this._socket.once('close', this._onSocketClose);
988
+ this._socket.once('end', this._onSocketEnd);
989
+ this._socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT); // 10 min.
990
+ this._socket.on('timeout', this._onSocketTimeout);
991
+ // resume in case the socket was paused
992
+ socketPlain.resume();
993
+ }
994
+ /**
995
+ * Processes queued responses from the server
996
+ * @internal
997
+ */
998
+ _processResponse() {
999
+ if (!this._responseQueue.length) {
1000
+ return false;
1001
+ }
1002
+ const raw = (this._responseQueue.shift() || '').toString();
1003
+ // Skip unexpected empty lines without consuming a response action or
1004
+ // overwriting lastServerResponse; reprocess whatever else is queued.
1005
+ if (!raw.trim()) {
1006
+ setImmediate(() => this._processResponse());
1007
+ return;
1008
+ }
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
1013
+ this._responseQueue.unshift(raw);
1014
+ return;
1015
+ }
1016
+ const str = (this.lastServerResponse = decodeServerResponse(raw));
1017
+ if (this.options.debug || this.options.transactionLog) {
1018
+ this.logger.debug({
1019
+ tnx: 'server'
1020
+ }, str.replace(/\r?\n$/, ''));
1021
+ }
1022
+ const action = this._responseActions.shift();
1023
+ if (typeof action === 'function') {
1024
+ action.call(this, str);
1025
+ setImmediate(() => this._processResponse());
1026
+ }
1027
+ else {
1028
+ return this._onError(new Error('Unexpected Response'), 'EPROTOCOL', str, 'CONN');
1029
+ }
1030
+ }
1031
+ /**
1032
+ * Send a command to the server, append \r\n
1033
+ *
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
1037
+ */
1038
+ _sendCommand(str, logStr) {
1039
+ if (this._destroyed) {
1040
+ // Connection already closed, can't send any more data
1041
+ return;
1042
+ }
1043
+ const socket = this._socket;
1044
+ if (socket.destroyed) {
1045
+ return this.close();
1046
+ }
1047
+ if (this.options.debug || this.options.transactionLog) {
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'));
1053
+ }
1054
+ /**
1055
+ * Initiates a new message by submitting envelope data, starting with
1056
+ * MAIL FROM: command
1057
+ *
1058
+ * @param envelope Envelope object in the form of
1059
+ * {from:'...', to:['...']}
1060
+ * or
1061
+ * {from:{address:'...',name:'...'}, to:[address:'...',name:'...']}
1062
+ * @param callback Callback to run once the envelope is processed
1063
+ * @internal
1064
+ */
1065
+ _setEnvelope(envelope, callback) {
1066
+ const args = [];
1067
+ let useSmtpUtf8 = false;
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());
1075
+ if (!this._envelope.to.length) {
1076
+ return callback(this._formatError('No recipients defined', 'EENVELOPE', false, 'API'));
1077
+ }
1078
+ if (this._envelope.from && /[\r\n<>]/.test(this._envelope.from)) {
1079
+ return callback(this._formatError('Invalid sender ' + JSON.stringify(this._envelope.from), 'EENVELOPE', false, 'API'));
1080
+ }
1081
+ // check if the sender address uses only ASCII characters,
1082
+ // otherwise require usage of SMTPUTF8 extension
1083
+ if (/[\x80-\uFFFF]/.test(this._envelope.from)) {
1084
+ useSmtpUtf8 = true;
1085
+ }
1086
+ for (let i = 0, len = this._envelope.to.length; i < len; i++) {
1087
+ if (!this._envelope.to[i] || /[\r\n<>]/.test(this._envelope.to[i])) {
1088
+ return callback(this._formatError('Invalid recipient ' + JSON.stringify(this._envelope.to[i]), 'EENVELOPE', false, 'API'));
1089
+ }
1090
+ // check if the recipients addresses use only ASCII characters,
1091
+ // otherwise require usage of SMTPUTF8 extension
1092
+ if (/[\x80-\uFFFF]/.test(this._envelope.to[i])) {
1093
+ useSmtpUtf8 = true;
1094
+ }
1095
+ }
1096
+ // clone the recipients array for latter manipulation
1097
+ this._envelope.rcptQueue = [].concat(this._envelope.to || []);
1098
+ this._envelope.rejected = [];
1099
+ this._envelope.rejectedErrors = [];
1100
+ this._envelope.accepted = [];
1101
+ if (this._envelope.dsn) {
1102
+ try {
1103
+ this._envelope.dsn = this._setDsnEnvelope(this._envelope.dsn);
1104
+ }
1105
+ catch (err) {
1106
+ return callback(this._formatError('Invalid DSN ' + err.message, 'EENVELOPE', false, 'API'));
1107
+ }
1108
+ }
1109
+ // RFC 8689: validate REQUIRETLS eligibility before queuing the MAIL FROM
1110
+ // response action, so a rejection here cannot leave an orphaned action in
1111
+ // _responseActions (which would consume the next reply and desync a reused
1112
+ // connection).
1113
+ if (this._envelope.requireTLSExtensionEnabled) {
1114
+ if (!this.secure) {
1115
+ return callback(this._formatError('REQUIRETLS can only be used over TLS connections (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM'));
1116
+ }
1117
+ if (!this._supportedExtensions.includes('REQUIRETLS')) {
1118
+ return callback(this._formatError('Server does not support REQUIRETLS extension (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM'));
1119
+ }
1120
+ }
1121
+ this._responseActions.push(str => {
1122
+ this._actionMAIL(str, callback);
1123
+ });
1124
+ // If the server supports SMTPUTF8 and the envelope includes an internationalized
1125
+ // email address then append SMTPUTF8 keyword to the MAIL FROM command
1126
+ if (useSmtpUtf8 && this._supportedExtensions.includes('SMTPUTF8')) {
1127
+ args.push('SMTPUTF8');
1128
+ this._usingSmtpUtf8 = true;
1129
+ }
1130
+ // If the server supports 8BITMIME and the message might contain non-ascii bytes
1131
+ // then append the 8BITMIME keyword to the MAIL FROM command
1132
+ if (this._envelope.use8BitMime && this._supportedExtensions.includes('8BITMIME')) {
1133
+ args.push('BODY=8BITMIME');
1134
+ this._using8BitMime = true;
1135
+ }
1136
+ if (this._envelope.size && this._supportedExtensions.includes('SIZE')) {
1137
+ const sizeValue = Number(this._envelope.size) || 0;
1138
+ if (sizeValue > 0) {
1139
+ args.push('SIZE=' + sizeValue);
1140
+ }
1141
+ }
1142
+ // If the server supports DSN and the envelope includes an DSN prop
1143
+ // then append DSN params to the MAIL FROM command
1144
+ if (this._envelope.dsn && this._supportedExtensions.includes('DSN')) {
1145
+ if (this._envelope.dsn.ret) {
1146
+ args.push('RET=' + shared.encodeXText(this._envelope.dsn.ret));
1147
+ }
1148
+ if (this._envelope.dsn.envid) {
1149
+ args.push('ENVID=' + shared.encodeXText(this._envelope.dsn.envid));
1150
+ }
1151
+ }
1152
+ // RFC 8689: append the REQUIRETLS keyword to MAIL FROM. Eligibility
1153
+ // (TLS connection + server support) was already validated above, before
1154
+ // the response action was queued.
1155
+ if (this._envelope.requireTLSExtensionEnabled) {
1156
+ args.push('REQUIRETLS');
1157
+ }
1158
+ this._sendCommand('MAIL FROM:<' + this._envelope.from + '>' + (args.length ? ' ' + args.join(' ') : ''));
1159
+ }
1160
+ /** @internal */
1161
+ _setDsnEnvelope(params) {
1162
+ let ret = (params.ret || params.return || '').toString().toUpperCase() || null;
1163
+ if (ret) {
1164
+ switch (ret) {
1165
+ case 'HDRS':
1166
+ case 'HEADERS':
1167
+ ret = 'HDRS';
1168
+ break;
1169
+ case 'FULL':
1170
+ case 'BODY':
1171
+ ret = 'FULL';
1172
+ break;
1173
+ }
1174
+ }
1175
+ if (ret && !['FULL', 'HDRS'].includes(ret)) {
1176
+ throw new Error('ret: ' + JSON.stringify(ret));
1177
+ }
1178
+ const envid = (params.envid || params.id || '').toString() || null;
1179
+ let notify = params.notify || null;
1180
+ if (notify) {
1181
+ if (typeof notify === 'string') {
1182
+ notify = notify.split(',');
1183
+ }
1184
+ notify = notify.map(n => n.trim().toUpperCase());
1185
+ const validNotify = ['NEVER', 'SUCCESS', 'FAILURE', 'DELAY'];
1186
+ const invalidNotify = notify.filter(n => !validNotify.includes(n));
1187
+ if (invalidNotify.length || (notify.length > 1 && notify.includes('NEVER'))) {
1188
+ throw new Error('notify: ' + JSON.stringify(notify.join(',')));
1189
+ }
1190
+ notify = notify.join(',');
1191
+ }
1192
+ let orcpt = (params.recipient || params.orcpt || '').toString() || null;
1193
+ if (orcpt && orcpt.indexOf(';') < 0) {
1194
+ orcpt = 'rfc822;' + orcpt;
1195
+ }
1196
+ return {
1197
+ ret,
1198
+ envid,
1199
+ notify,
1200
+ orcpt
1201
+ };
1202
+ }
1203
+ /** @internal */
1204
+ _getDsnRcptToArgs() {
1205
+ const envelope = this._envelope;
1206
+ const args = [];
1207
+ // If the server supports DSN and the envelope includes an DSN prop
1208
+ // then append DSN params to the RCPT TO command
1209
+ if (envelope.dsn && this._supportedExtensions.includes('DSN')) {
1210
+ if (envelope.dsn.notify) {
1211
+ args.push('NOTIFY=' + shared.encodeXText(envelope.dsn.notify));
1212
+ }
1213
+ if (envelope.dsn.orcpt) {
1214
+ args.push('ORCPT=' + shared.encodeXText(envelope.dsn.orcpt));
1215
+ }
1216
+ }
1217
+ return args.length ? ' ' + args.join(' ') : '';
1218
+ }
1219
+ /** @internal */
1220
+ _createSendStream(callback) {
1221
+ const envelope = this._envelope;
1222
+ const dataStream = new DataStream();
1223
+ if (this.options.lmtp) {
1224
+ envelope.accepted.forEach((recipient, i) => {
1225
+ const final = i === envelope.accepted.length - 1;
1226
+ this._responseActions.push(str => {
1227
+ this._actionLMTPStream(recipient, final, str, callback);
1228
+ });
1229
+ });
1230
+ }
1231
+ else {
1232
+ this._responseActions.push(str => {
1233
+ this._actionSMTPStream(str, callback);
1234
+ });
1235
+ }
1236
+ this._currentDataStream = dataStream;
1237
+ dataStream.pipe(this._socket, {
1238
+ end: false
1239
+ });
1240
+ if (this.options.debug) {
1241
+ const logStream = new PassThrough();
1242
+ logStream.on('readable', () => {
1243
+ let chunk;
1244
+ while ((chunk = logStream.read())) {
1245
+ this.logger.debug({
1246
+ tnx: 'message'
1247
+ }, chunk.toString('binary').replace(/\r?\n$/, ''));
1248
+ }
1249
+ });
1250
+ dataStream.pipe(logStream);
1251
+ }
1252
+ dataStream.once('end', () => {
1253
+ if (this._currentDataStream === dataStream) {
1254
+ this._currentDataStream = false;
1255
+ }
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);
1261
+ });
1262
+ return dataStream;
1263
+ }
1264
+ /** ACTIONS **/
1265
+ /**
1266
+ * Will be run after the connection is created and the server sends
1267
+ * a greeting. If the incoming message starts with 220 initiate
1268
+ * SMTP session by sending EHLO command
1269
+ *
1270
+ * @param str Message from the server
1271
+ * @internal
1272
+ */
1273
+ _actionGreeting(str) {
1274
+ clearTimeout(this._greetingTimeout);
1275
+ if (str.substr(0, 3) !== '220') {
1276
+ this._onError(new Error('Invalid greeting. response=' + str), 'EPROTOCOL', str, 'CONN');
1277
+ return;
1278
+ }
1279
+ if (this.options.lmtp) {
1280
+ this._responseActions.push(this._actionLHLO);
1281
+ this._sendCommand('LHLO ' + this.name);
1282
+ }
1283
+ else {
1284
+ this._responseActions.push(this._actionEHLO);
1285
+ this._sendCommand('EHLO ' + this.name);
1286
+ }
1287
+ }
1288
+ /**
1289
+ * Handles server response for LHLO command. If it yielded in
1290
+ * error, emit 'error', otherwise treat this as an EHLO response
1291
+ *
1292
+ * @param str Message from the server
1293
+ * @internal
1294
+ */
1295
+ _actionLHLO(str) {
1296
+ if (str.charAt(0) !== '2') {
1297
+ this._onError(new Error('Invalid LHLO. response=' + str), 'EPROTOCOL', str, 'LHLO');
1298
+ return;
1299
+ }
1300
+ this._actionEHLO(str);
1301
+ }
1302
+ /**
1303
+ * Handles server response for EHLO command. If it yielded in
1304
+ * error, try HELO instead, otherwise initiate TLS negotiation
1305
+ * if STARTTLS is supported by the server or move into the
1306
+ * authentication phase.
1307
+ *
1308
+ * @param str Message from the server
1309
+ * @internal
1310
+ */
1311
+ _actionEHLO(str) {
1312
+ let match;
1313
+ if (str.substr(0, 3) === '421') {
1314
+ this._onError(new Error('Server terminates connection. response=' + str), 'ECONNECTION', str, 'EHLO');
1315
+ return;
1316
+ }
1317
+ if (str.charAt(0) !== '2') {
1318
+ if (this.options.requireTLS) {
1319
+ this._onError(new Error('EHLO failed but HELO does not support required STARTTLS. response=' + str), 'ECONNECTION', str, 'EHLO');
1320
+ return;
1321
+ }
1322
+ // Try HELO instead
1323
+ this._responseActions.push(this._actionHELO);
1324
+ this._sendCommand('HELO ' + this.name);
1325
+ return;
1326
+ }
1327
+ this._ehloLines = str
1328
+ .split(/\r?\n/)
1329
+ .map(line => line.replace(/^\d+[ -]/, '').trim())
1330
+ .filter(line => line)
1331
+ .slice(1);
1332
+ // Detect if the server supports STARTTLS
1333
+ if (!this.secure && !this.options.ignoreTLS && (/[ -]STARTTLS\b/im.test(str) || this.options.requireTLS)) {
1334
+ this._sendCommand('STARTTLS');
1335
+ this._responseActions.push(this._actionSTARTTLS);
1336
+ return;
1337
+ }
1338
+ // Detect if the server supports SMTPUTF8
1339
+ if (/[ -]SMTPUTF8\b/im.test(str)) {
1340
+ this._supportedExtensions.push('SMTPUTF8');
1341
+ }
1342
+ // Detect if the server supports DSN
1343
+ if (/[ -]DSN\b/im.test(str)) {
1344
+ this._supportedExtensions.push('DSN');
1345
+ }
1346
+ // Detect if the server supports 8BITMIME
1347
+ if (/[ -]8BITMIME\b/im.test(str)) {
1348
+ this._supportedExtensions.push('8BITMIME');
1349
+ }
1350
+ // Detect if the server supports REQUIRETLS (RFC 8689)
1351
+ if (/[ -]REQUIRETLS\b/im.test(str)) {
1352
+ this._supportedExtensions.push('REQUIRETLS');
1353
+ }
1354
+ // Detect if the server supports PIPELINING
1355
+ if (/[ -]PIPELINING\b/im.test(str)) {
1356
+ this._supportedExtensions.push('PIPELINING');
1357
+ }
1358
+ // Detect if the server supports AUTH
1359
+ if (/[ -]AUTH\b/i.test(str)) {
1360
+ this.allowsAuth = true;
1361
+ }
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
+ }
1375
+ }
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
+ }
1381
+ }
1382
+ // Detect if the server supports SIZE extensions (and the max allowed size)
1383
+ if ((match = str.match(/[ -]SIZE(?:[ \t]+(\d+))?/im))) {
1384
+ this._supportedExtensions.push('SIZE');
1385
+ this._maxAllowedSize = Number(match[1]) || 0;
1386
+ }
1387
+ this.emit('connect');
1388
+ }
1389
+ /**
1390
+ * Handles server response for HELO command. If it yielded in
1391
+ * error, emit 'error', otherwise move into the authentication phase.
1392
+ *
1393
+ * @param str Message from the server
1394
+ * @internal
1395
+ */
1396
+ _actionHELO(str) {
1397
+ if (str.charAt(0) !== '2') {
1398
+ this._onError(new Error('Invalid HELO. response=' + str), 'EPROTOCOL', str, 'HELO');
1399
+ return;
1400
+ }
1401
+ // assume that authentication is enabled (most probably is not though)
1402
+ this.allowsAuth = true;
1403
+ this.emit('connect');
1404
+ }
1405
+ /**
1406
+ * Handles server response for STARTTLS command. If there's an error
1407
+ * try HELO instead, otherwise initiate TLS upgrade. If the upgrade
1408
+ * succeedes restart the EHLO
1409
+ *
1410
+ * @param str Message from the server
1411
+ * @internal
1412
+ */
1413
+ _actionSTARTTLS(str) {
1414
+ if (str.charAt(0) !== '2') {
1415
+ if (this.options.opportunisticTLS) {
1416
+ this.logger.info({
1417
+ tnx: 'smtp'
1418
+ }, 'Failed STARTTLS upgrade, continuing unencrypted');
1419
+ this.emit('connect');
1420
+ return;
1421
+ }
1422
+ this._onError(new Error('Error upgrading connection with STARTTLS'), 'ETLS', str, 'STARTTLS');
1423
+ return;
1424
+ }
1425
+ this._upgradeConnection((err, secured) => {
1426
+ if (err) {
1427
+ this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'STARTTLS');
1428
+ return;
1429
+ }
1430
+ this.logger.info({
1431
+ tnx: 'smtp'
1432
+ }, 'Connection upgraded with STARTTLS');
1433
+ if (secured) {
1434
+ // restart session
1435
+ if (this.options.lmtp) {
1436
+ this._responseActions.push(this._actionLHLO);
1437
+ this._sendCommand('LHLO ' + this.name);
1438
+ }
1439
+ else {
1440
+ this._responseActions.push(this._actionEHLO);
1441
+ this._sendCommand('EHLO ' + this.name);
1442
+ }
1443
+ }
1444
+ else {
1445
+ this.emit('connect');
1446
+ }
1447
+ });
1448
+ }
1449
+ /**
1450
+ * Handle the response for AUTH LOGIN command. We are expecting
1451
+ * '334 VXNlcm5hbWU6' (base64 for 'Username:'). Data to be sent as
1452
+ * response needs to be base64 encoded username. We do not need
1453
+ * exact match but settle with 334 response in general as some
1454
+ * hosts invalidly use a longer message than VXNlcm5hbWU6
1455
+ *
1456
+ * @param str Message from the server
1457
+ * @param callback Callback to run once the authentication sequence completes
1458
+ * @internal
1459
+ */
1460
+ _actionAUTH_LOGIN_USER(str, callback) {
1461
+ if (!/^334[ -]/.test(str)) {
1462
+ // expecting '334 VXNlcm5hbWU6'
1463
+ callback(this._formatError('Invalid login sequence while waiting for "334 VXNlcm5hbWU6"', 'EAUTH', str, 'AUTH LOGIN'));
1464
+ return;
1465
+ }
1466
+ this._responseActions.push(str => {
1467
+ this._actionAUTH_LOGIN_PASS(str, callback);
1468
+ });
1469
+ this._sendCommand(Buffer.from(this._auth.credentials.user + '', 'utf-8').toString('base64'));
1470
+ }
1471
+ /**
1472
+ * Handle the response for AUTH CRAM-MD5 command. We are expecting
1473
+ * '334 <challenge string>'. Data to be sent as response needs to be
1474
+ * base64 decoded challenge string, MD5 hashed using the password as
1475
+ * a HMAC key, prefixed by the username and a space, and finally all
1476
+ * base64 encoded again.
1477
+ *
1478
+ * @param str Message from the server
1479
+ * @param callback Callback to run once the authentication sequence completes
1480
+ * @internal
1481
+ */
1482
+ _actionAUTH_CRAM_MD5(str, callback) {
1483
+ const challengeMatch = str.match(/^334\s+(.+)$/);
1484
+ if (!challengeMatch) {
1485
+ return callback(this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH CRAM-MD5'));
1486
+ }
1487
+ // Decode from base64
1488
+ const base64decoded = Buffer.from(challengeMatch[1], 'base64').toString('ascii');
1489
+ const hmacMD5 = crypto.createHmac('md5', this._auth.credentials.pass);
1490
+ hmacMD5.update(base64decoded);
1491
+ const prepended = this._auth.credentials.user + ' ' + hmacMD5.digest('hex');
1492
+ this._responseActions.push(str => {
1493
+ this._actionAUTH_CRAM_MD5_PASS(str, callback);
1494
+ });
1495
+ this._sendCommand(Buffer.from(prepended).toString('base64'),
1496
+ // hidden hash for logs
1497
+ Buffer.from(this._auth.credentials.user + ' /* secret */').toString('base64'));
1498
+ }
1499
+ /**
1500
+ * Handles the response to CRAM-MD5 authentication, if there's no error,
1501
+ * the user can be considered logged in. Start waiting for a message to send
1502
+ *
1503
+ * @param str Message from the server
1504
+ * @param callback Callback to run once the authentication sequence completes
1505
+ * @internal
1506
+ */
1507
+ _actionAUTH_CRAM_MD5_PASS(str, callback) {
1508
+ if (!str.match(/^235\s+/)) {
1509
+ return callback(this._formatError('Invalid login sequence while waiting for "235"', 'EAUTH', str, 'AUTH CRAM-MD5'));
1510
+ }
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));
1517
+ this.authenticated = true;
1518
+ callback(null, true);
1519
+ }
1520
+ /**
1521
+ * Handle the response for AUTH LOGIN command. We are expecting
1522
+ * '334 UGFzc3dvcmQ6' (base64 for 'Password:'). Data to be sent as
1523
+ * response needs to be base64 encoded password.
1524
+ *
1525
+ * @param str Message from the server
1526
+ * @param callback Callback to run once the authentication sequence completes
1527
+ * @internal
1528
+ */
1529
+ _actionAUTH_LOGIN_PASS(str, callback) {
1530
+ if (!/^334[ -]/.test(str)) {
1531
+ // expecting '334 UGFzc3dvcmQ6'
1532
+ return callback(this._formatError('Invalid login sequence while waiting for "334 UGFzc3dvcmQ6"', 'EAUTH', str, 'AUTH LOGIN'));
1533
+ }
1534
+ this._responseActions.push(str => {
1535
+ this._actionAUTHComplete(str, callback);
1536
+ });
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'));
1540
+ }
1541
+ /**
1542
+ * Handles the response for authentication, if there's no error,
1543
+ * the user can be considered logged in. Start waiting for a message to send
1544
+ *
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
1549
+ */
1550
+ _actionAUTHComplete(str, isRetry, callback) {
1551
+ if (!callback && typeof isRetry === 'function') {
1552
+ callback = isRetry;
1553
+ isRetry = false;
1554
+ }
1555
+ if (str.substr(0, 3) === '334') {
1556
+ this._responseActions.push(str => {
1557
+ if (isRetry || this._authMethod !== 'XOAUTH2') {
1558
+ this._actionAUTHComplete(str, true, callback);
1559
+ }
1560
+ else {
1561
+ // fetch a new OAuth2 access token
1562
+ setImmediate(() => this._handleXOauth2Token(true, callback));
1563
+ }
1564
+ });
1565
+ this._sendCommand('');
1566
+ return;
1567
+ }
1568
+ if (str.charAt(0) !== '2') {
1569
+ this.logger.info({
1570
+ tnx: 'smtp',
1571
+ username: this._auth.user,
1572
+ action: 'authfail',
1573
+ method: this._authMethod
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));
1583
+ this.authenticated = true;
1584
+ callback(null, true);
1585
+ }
1586
+ /**
1587
+ * Handle response for a MAIL FROM: command
1588
+ *
1589
+ * @param str Message from the server
1590
+ * @param callback Callback to run once the envelope is processed
1591
+ * @internal
1592
+ */
1593
+ _actionMAIL(str, callback) {
1594
+ const envelope = this._envelope;
1595
+ if (Number(str.charAt(0)) !== 2) {
1596
+ const message = this._usingSmtpUtf8 && /^550 /.test(str) && /[\x80-\uFFFF]/.test(envelope.from)
1597
+ ? 'Internationalized mailbox name not allowed'
1598
+ : 'Mail command failed';
1599
+ return callback(this._formatError(message, 'EENVELOPE', str, 'MAIL FROM'));
1600
+ }
1601
+ if (!envelope.rcptQueue.length) {
1602
+ return callback(this._formatError("Can't send mail - no recipients defined", 'EENVELOPE', false, 'API'));
1603
+ }
1604
+ this._recipientQueue = [];
1605
+ const usePipelining = this._supportedExtensions.includes('PIPELINING');
1606
+ do {
1607
+ const curRecipient = envelope.rcptQueue.shift();
1608
+ this._recipientQueue.push(curRecipient);
1609
+ this._responseActions.push(str => {
1610
+ this._actionRCPT(str, callback);
1611
+ });
1612
+ this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
1613
+ } while (usePipelining && envelope.rcptQueue.length);
1614
+ }
1615
+ /**
1616
+ * Handle response for a RCPT TO: command
1617
+ *
1618
+ * @param str Message from the server
1619
+ * @param callback Callback to run once the envelope is processed
1620
+ * @internal
1621
+ */
1622
+ _actionRCPT(str, callback) {
1623
+ const envelope = this._envelope;
1624
+ let err;
1625
+ const curRecipient = this._recipientQueue.shift();
1626
+ if (Number(str.charAt(0)) !== 2) {
1627
+ // this is a soft error
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);
1632
+ // store error for the failed recipient
1633
+ err = this._formatError(message, 'EENVELOPE', str, 'RCPT TO');
1634
+ err.recipient = curRecipient;
1635
+ envelope.rejectedErrors.push(err);
1636
+ }
1637
+ else {
1638
+ envelope.accepted.push(curRecipient);
1639
+ }
1640
+ if (!envelope.rcptQueue.length && !this._recipientQueue.length) {
1641
+ if (envelope.rejected.length < envelope.to.length) {
1642
+ this._responseActions.push(str => {
1643
+ this._actionDATA(str, callback);
1644
+ });
1645
+ this._sendCommand('DATA');
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;
1655
+ return callback(err);
1656
+ }
1657
+ }
1658
+ else if (envelope.rcptQueue.length) {
1659
+ const nextRecipient = envelope.rcptQueue.shift();
1660
+ this._recipientQueue.push(nextRecipient);
1661
+ this._responseActions.push(str => {
1662
+ this._actionRCPT(str, callback);
1663
+ });
1664
+ this._sendCommand('RCPT TO:<' + nextRecipient + '>' + this._getDsnRcptToArgs());
1665
+ }
1666
+ }
1667
+ /**
1668
+ * Handle response for a DATA command
1669
+ *
1670
+ * @param str Message from the server
1671
+ * @param callback Callback to run once the envelope is processed
1672
+ * @internal
1673
+ */
1674
+ _actionDATA(str, callback) {
1675
+ const envelope = this._envelope;
1676
+ // response should be 354 but according to this issue https://github.com/eleith/emailjs/issues/24
1677
+ // some servers might use 250 instead, so lets check for 2 or 3 as the first digit
1678
+ if (!/^[23]/.test(str)) {
1679
+ return callback(this._formatError('Data command failed', 'EENVELOPE', str, 'DATA'));
1680
+ }
1681
+ const response = {
1682
+ accepted: envelope.accepted,
1683
+ rejected: envelope.rejected
1684
+ };
1685
+ if (this._ehloLines && this._ehloLines.length) {
1686
+ response.ehlo = this._ehloLines;
1687
+ }
1688
+ if (envelope.rejectedErrors.length) {
1689
+ response.rejectedErrors = envelope.rejectedErrors;
1690
+ }
1691
+ callback(null, response);
1692
+ }
1693
+ /**
1694
+ * Handle response for a DATA stream when using SMTP
1695
+ * We expect a single response that defines if the sending succeeded or failed
1696
+ *
1697
+ * @param str Message from the server
1698
+ * @param callback Callback to run with the final send result
1699
+ * @internal
1700
+ */
1701
+ _actionSMTPStream(str, callback) {
1702
+ if (Number(str.charAt(0)) !== 2) {
1703
+ return callback(this._formatError('Message failed', 'EMESSAGE', str, 'DATA'));
1704
+ }
1705
+ return callback(null, str);
1706
+ }
1707
+ /**
1708
+ * Handle response for a DATA stream
1709
+ * We expect a separate response for every recipient. All recipients can either
1710
+ * succeed or fail separately
1711
+ *
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
1717
+ */
1718
+ _actionLMTPStream(recipient, final, str, callback) {
1719
+ const envelope = this._envelope;
1720
+ let err;
1721
+ if (Number(str.charAt(0)) !== 2) {
1722
+ // Message failed
1723
+ err = this._formatError('Message failed for recipient ' + recipient, 'EMESSAGE', str, 'DATA');
1724
+ err.recipient = recipient;
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);
1730
+ }
1731
+ }
1732
+ }
1733
+ if (final) {
1734
+ return callback(null, str);
1735
+ }
1736
+ }
1737
+ /** @internal */
1738
+ _handleXOauth2Token(isRetry, callback) {
1739
+ this._auth.oauth2.getToken(isRetry, (err, accessToken) => {
1740
+ if (err) {
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));
1747
+ return callback(this._formatError(err, 'EAUTH', false, 'AUTH XOAUTH2'));
1748
+ }
1749
+ this._responseActions.push(str => {
1750
+ this._actionAUTHComplete(str, isRetry, callback);
1751
+ });
1752
+ this._sendCommand('AUTH XOAUTH2 ' + this._auth.oauth2.buildXOAuth2Token(accessToken),
1753
+ // Hidden for logs
1754
+ 'AUTH XOAUTH2 ' + this._auth.oauth2.buildXOAuth2Token('/* secret */'));
1755
+ });
1756
+ }
1757
+ /**
1758
+ *
1759
+ * @param command
1760
+ * @internal
1761
+ */
1762
+ _isDestroyedMessage(command) {
1763
+ if (this._destroyed) {
1764
+ return 'Cannot ' + command + ' - smtp connection is already destroyed.';
1765
+ }
1766
+ if (this._socket) {
1767
+ if (this._socket.destroyed) {
1768
+ return 'Cannot ' + command + ' - smtp connection socket is already destroyed.';
1769
+ }
1770
+ if (!this._socket.writable) {
1771
+ return 'Cannot ' + command + ' - smtp connection socket is already half-closed.';
1772
+ }
1773
+ }
1774
+ }
1775
+ /** @internal */
1776
+ _getHostname() {
1777
+ // defaul hostname is machine hostname or [IP]
1778
+ let defaultHostname;
1779
+ try {
1780
+ defaultHostname = os.hostname() || '';
1781
+ }
1782
+ catch (_err) {
1783
+ // fails on windows 7
1784
+ defaultHostname = 'localhost';
1785
+ }
1786
+ // ignore if not FQDN
1787
+ if (!defaultHostname || defaultHostname.indexOf('.') < 0) {
1788
+ defaultHostname = '[127.0.0.1]';
1789
+ }
1790
+ // IP should be enclosed in []
1791
+ if (defaultHostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
1792
+ defaultHostname = '[' + defaultHostname + ']';
1793
+ }
1794
+ return defaultHostname;
1795
+ }
1796
+ }
1797
+ export default SMTPConnection;