@nocobase/plugin-notification-email 1.4.0-alpha.20240928155737

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