polikolog 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. package/.idea/5lab.iml +12 -0
  2. package/.idea/inspectionProfiles/Project_Default.xml +10 -0
  3. package/.idea/jsLibraryMappings.xml +6 -0
  4. package/.idea/modules.xml +8 -0
  5. package/.idea/vcs.xml +6 -0
  6. package/06-02.js +48 -0
  7. package/06-03.js +22 -0
  8. package/06-04.js +22 -0
  9. package/index.html +41 -0
  10. package/m0603.js +28 -0
  11. package/mypackage/m0603.js +28 -0
  12. package/mypackage/node_modules/.package-lock.json +24 -0
  13. package/mypackage/node_modules/nodemailer/.gitattributes +6 -0
  14. package/mypackage/node_modules/nodemailer/.prettierrc.js +8 -0
  15. package/mypackage/node_modules/nodemailer/CHANGELOG.md +725 -0
  16. package/mypackage/node_modules/nodemailer/CODE_OF_CONDUCT.md +76 -0
  17. package/mypackage/node_modules/nodemailer/CONTRIBUTING.md +67 -0
  18. package/mypackage/node_modules/nodemailer/LICENSE +16 -0
  19. package/mypackage/node_modules/nodemailer/README.md +97 -0
  20. package/mypackage/node_modules/nodemailer/SECURITY.txt +22 -0
  21. package/mypackage/node_modules/nodemailer/lib/addressparser/index.js +313 -0
  22. package/mypackage/node_modules/nodemailer/lib/base64/index.js +142 -0
  23. package/mypackage/node_modules/nodemailer/lib/dkim/index.js +251 -0
  24. package/mypackage/node_modules/nodemailer/lib/dkim/message-parser.js +155 -0
  25. package/mypackage/node_modules/nodemailer/lib/dkim/relaxed-body.js +154 -0
  26. package/mypackage/node_modules/nodemailer/lib/dkim/sign.js +117 -0
  27. package/mypackage/node_modules/nodemailer/lib/fetch/cookies.js +281 -0
  28. package/mypackage/node_modules/nodemailer/lib/fetch/index.js +274 -0
  29. package/mypackage/node_modules/nodemailer/lib/json-transport/index.js +82 -0
  30. package/mypackage/node_modules/nodemailer/lib/mail-composer/index.js +558 -0
  31. package/mypackage/node_modules/nodemailer/lib/mailer/index.js +427 -0
  32. package/mypackage/node_modules/nodemailer/lib/mailer/mail-message.js +315 -0
  33. package/mypackage/node_modules/nodemailer/lib/mime-funcs/index.js +625 -0
  34. package/mypackage/node_modules/nodemailer/lib/mime-funcs/mime-types.js +2102 -0
  35. package/mypackage/node_modules/nodemailer/lib/mime-node/index.js +1290 -0
  36. package/mypackage/node_modules/nodemailer/lib/mime-node/last-newline.js +33 -0
  37. package/mypackage/node_modules/nodemailer/lib/mime-node/le-unix.js +43 -0
  38. package/mypackage/node_modules/nodemailer/lib/mime-node/le-windows.js +52 -0
  39. package/mypackage/node_modules/nodemailer/lib/nodemailer.js +143 -0
  40. package/mypackage/node_modules/nodemailer/lib/qp/index.js +219 -0
  41. package/mypackage/node_modules/nodemailer/lib/sendmail-transport/index.js +210 -0
  42. package/mypackage/node_modules/nodemailer/lib/ses-transport/index.js +349 -0
  43. package/mypackage/node_modules/nodemailer/lib/shared/index.js +638 -0
  44. package/mypackage/node_modules/nodemailer/lib/smtp-connection/data-stream.js +108 -0
  45. package/mypackage/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +143 -0
  46. package/mypackage/node_modules/nodemailer/lib/smtp-connection/index.js +1796 -0
  47. package/mypackage/node_modules/nodemailer/lib/smtp-pool/index.js +648 -0
  48. package/mypackage/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +253 -0
  49. package/mypackage/node_modules/nodemailer/lib/smtp-transport/index.js +416 -0
  50. package/mypackage/node_modules/nodemailer/lib/stream-transport/index.js +135 -0
  51. package/mypackage/node_modules/nodemailer/lib/well-known/index.js +47 -0
  52. package/mypackage/node_modules/nodemailer/lib/well-known/services.json +286 -0
  53. package/mypackage/node_modules/nodemailer/lib/xoauth2/index.js +376 -0
  54. package/mypackage/node_modules/nodemailer/package.json +46 -0
  55. package/mypackage/node_modules/nodemailer/postinstall.js +101 -0
  56. package/mypackage/package.json +15 -0
  57. package/package.json +15 -0
@@ -0,0 +1,1796 @@
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) {
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
+ this.logger.info(
821
+ {
822
+ tnx: 'network'
823
+ },
824
+ 'Connection closed'
825
+ );
826
+
827
+ if (this.upgrading && !this._destroyed) {
828
+ return this._onError(new Error('Connection closed unexpectedly'), 'ETLS', false, 'CONN');
829
+ } else if (![this._actionGreeting, this.close].includes(this._responseActions[0]) && !this._destroyed) {
830
+ return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', false, 'CONN');
831
+ }
832
+
833
+ this._destroy();
834
+ }
835
+
836
+ /**
837
+ * 'end' listener for the socket
838
+ *
839
+ * @event
840
+ */
841
+ _onEnd() {
842
+ if (this._socket && !this._socket.destroyed) {
843
+ this._socket.destroy();
844
+ }
845
+ }
846
+
847
+ /**
848
+ * 'timeout' listener for the socket
849
+ *
850
+ * @event
851
+ */
852
+ _onTimeout() {
853
+ return this._onError(new Error('Timeout'), 'ETIMEDOUT', false, 'CONN');
854
+ }
855
+
856
+ /**
857
+ * Destroys the client, emits 'end'
858
+ */
859
+ _destroy() {
860
+ if (this._destroyed) {
861
+ return;
862
+ }
863
+ this._destroyed = true;
864
+ this.emit('end');
865
+ }
866
+
867
+ /**
868
+ * Upgrades the connection to TLS
869
+ *
870
+ * @param {Function} callback Callback function to run when the connection
871
+ * has been secured
872
+ */
873
+ _upgradeConnection(callback) {
874
+ // do not remove all listeners or it breaks node v0.10 as there's
875
+ // apparently a 'finish' event set that would be cleared as well
876
+
877
+ // we can safely keep 'error', 'end', 'close' etc. events
878
+ this._socket.removeListener('data', this._onSocketData); // incoming data is going to be gibberish from this point onwards
879
+ this._socket.removeListener('timeout', this._onSocketTimeout); // timeout will be re-set for the new socket object
880
+
881
+ let socketPlain = this._socket;
882
+ let opts = {
883
+ socket: this._socket,
884
+ host: this.host
885
+ };
886
+
887
+ Object.keys(this.options.tls || {}).forEach(key => {
888
+ opts[key] = this.options.tls[key];
889
+ });
890
+
891
+ this.upgrading = true;
892
+ // tls.connect is not an asynchronous function however it may still throw errors and requires to be wrapped with try/catch
893
+ try {
894
+ this._socket = tls.connect(opts, () => {
895
+ this.secure = true;
896
+ this.upgrading = false;
897
+ this._socket.on('data', this._onSocketData);
898
+
899
+ socketPlain.removeListener('close', this._onSocketClose);
900
+ socketPlain.removeListener('end', this._onSocketEnd);
901
+
902
+ return callback(null, true);
903
+ });
904
+ } catch (err) {
905
+ return callback(err);
906
+ }
907
+
908
+ this._socket.on('error', this._onSocketError);
909
+ this._socket.once('close', this._onSocketClose);
910
+ this._socket.once('end', this._onSocketEnd);
911
+
912
+ this._socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT); // 10 min.
913
+ this._socket.on('timeout', this._onSocketTimeout);
914
+
915
+ // resume in case the socket was paused
916
+ socketPlain.resume();
917
+ }
918
+
919
+ /**
920
+ * Processes queued responses from the server
921
+ *
922
+ * @param {Boolean} force If true, ignores _processing flag
923
+ */
924
+ _processResponse() {
925
+ if (!this._responseQueue.length) {
926
+ return false;
927
+ }
928
+
929
+ let str = (this.lastServerResponse = (this._responseQueue.shift() || '').toString());
930
+
931
+ if (/^\d+-/.test(str.split('\n').pop())) {
932
+ // keep waiting for the final part of multiline response
933
+ return;
934
+ }
935
+
936
+ if (this.options.debug || this.options.transactionLog) {
937
+ this.logger.debug(
938
+ {
939
+ tnx: 'server'
940
+ },
941
+ str.replace(/\r?\n$/, '')
942
+ );
943
+ }
944
+
945
+ if (!str.trim()) {
946
+ // skip unexpected empty lines
947
+ setImmediate(() => this._processResponse(true));
948
+ }
949
+
950
+ let action = this._responseActions.shift();
951
+
952
+ if (typeof action === 'function') {
953
+ action.call(this, str);
954
+ setImmediate(() => this._processResponse(true));
955
+ } else {
956
+ return this._onError(new Error('Unexpected Response'), 'EPROTOCOL', str, 'CONN');
957
+ }
958
+ }
959
+
960
+ /**
961
+ * Send a command to the server, append \r\n
962
+ *
963
+ * @param {String} str String to be sent to the server
964
+ * @param {String} logStr Optional string to be used for logging instead of the actual string
965
+ */
966
+ _sendCommand(str, logStr) {
967
+ if (this._destroyed) {
968
+ // Connection already closed, can't send any more data
969
+ return;
970
+ }
971
+
972
+ if (this._socket.destroyed) {
973
+ return this.close();
974
+ }
975
+
976
+ if (this.options.debug || this.options.transactionLog) {
977
+ this.logger.debug(
978
+ {
979
+ tnx: 'client'
980
+ },
981
+ (logStr || str || '').toString().replace(/\r?\n$/, '')
982
+ );
983
+ }
984
+
985
+ this._socket.write(Buffer.from(str + '\r\n', 'utf-8'));
986
+ }
987
+
988
+ /**
989
+ * Initiates a new message by submitting envelope data, starting with
990
+ * MAIL FROM: command
991
+ *
992
+ * @param {Object} envelope Envelope object in the form of
993
+ * {from:'...', to:['...']}
994
+ * or
995
+ * {from:{address:'...',name:'...'}, to:[address:'...',name:'...']}
996
+ */
997
+ _setEnvelope(envelope, callback) {
998
+ let args = [];
999
+ let useSmtpUtf8 = false;
1000
+
1001
+ this._envelope = envelope || {};
1002
+ this._envelope.from = ((this._envelope.from && this._envelope.from.address) || this._envelope.from || '').toString().trim();
1003
+
1004
+ this._envelope.to = [].concat(this._envelope.to || []).map(to => ((to && to.address) || to || '').toString().trim());
1005
+
1006
+ if (!this._envelope.to.length) {
1007
+ return callback(this._formatError('No recipients defined', 'EENVELOPE', false, 'API'));
1008
+ }
1009
+
1010
+ if (this._envelope.from && /[\r\n<>]/.test(this._envelope.from)) {
1011
+ return callback(this._formatError('Invalid sender ' + JSON.stringify(this._envelope.from), 'EENVELOPE', false, 'API'));
1012
+ }
1013
+
1014
+ // check if the sender address uses only ASCII characters,
1015
+ // otherwise require usage of SMTPUTF8 extension
1016
+ if (/[\x80-\uFFFF]/.test(this._envelope.from)) {
1017
+ useSmtpUtf8 = true;
1018
+ }
1019
+
1020
+ for (let i = 0, len = this._envelope.to.length; i < len; i++) {
1021
+ if (!this._envelope.to[i] || /[\r\n<>]/.test(this._envelope.to[i])) {
1022
+ return callback(this._formatError('Invalid recipient ' + JSON.stringify(this._envelope.to[i]), 'EENVELOPE', false, 'API'));
1023
+ }
1024
+
1025
+ // check if the recipients addresses use only ASCII characters,
1026
+ // otherwise require usage of SMTPUTF8 extension
1027
+ if (/[\x80-\uFFFF]/.test(this._envelope.to[i])) {
1028
+ useSmtpUtf8 = true;
1029
+ }
1030
+ }
1031
+
1032
+ // clone the recipients array for latter manipulation
1033
+ this._envelope.rcptQueue = JSON.parse(JSON.stringify(this._envelope.to || []));
1034
+ this._envelope.rejected = [];
1035
+ this._envelope.rejectedErrors = [];
1036
+ this._envelope.accepted = [];
1037
+
1038
+ if (this._envelope.dsn) {
1039
+ try {
1040
+ this._envelope.dsn = this._setDsnEnvelope(this._envelope.dsn);
1041
+ } catch (err) {
1042
+ return callback(this._formatError('Invalid DSN ' + err.message, 'EENVELOPE', false, 'API'));
1043
+ }
1044
+ }
1045
+
1046
+ this._responseActions.push(str => {
1047
+ this._actionMAIL(str, callback);
1048
+ });
1049
+
1050
+ // If the server supports SMTPUTF8 and the envelope includes an internationalized
1051
+ // email address then append SMTPUTF8 keyword to the MAIL FROM command
1052
+ if (useSmtpUtf8 && this._supportedExtensions.includes('SMTPUTF8')) {
1053
+ args.push('SMTPUTF8');
1054
+ this._usingSmtpUtf8 = true;
1055
+ }
1056
+
1057
+ // If the server supports 8BITMIME and the message might contain non-ascii bytes
1058
+ // then append the 8BITMIME keyword to the MAIL FROM command
1059
+ if (this._envelope.use8BitMime && this._supportedExtensions.includes('8BITMIME')) {
1060
+ args.push('BODY=8BITMIME');
1061
+ this._using8BitMime = true;
1062
+ }
1063
+
1064
+ if (this._envelope.size && this._supportedExtensions.includes('SIZE')) {
1065
+ args.push('SIZE=' + this._envelope.size);
1066
+ }
1067
+
1068
+ // If the server supports DSN and the envelope includes an DSN prop
1069
+ // then append DSN params to the MAIL FROM command
1070
+ if (this._envelope.dsn && this._supportedExtensions.includes('DSN')) {
1071
+ if (this._envelope.dsn.ret) {
1072
+ args.push('RET=' + shared.encodeXText(this._envelope.dsn.ret));
1073
+ }
1074
+ if (this._envelope.dsn.envid) {
1075
+ args.push('ENVID=' + shared.encodeXText(this._envelope.dsn.envid));
1076
+ }
1077
+ }
1078
+
1079
+ this._sendCommand('MAIL FROM:<' + this._envelope.from + '>' + (args.length ? ' ' + args.join(' ') : ''));
1080
+ }
1081
+
1082
+ _setDsnEnvelope(params) {
1083
+ let ret = (params.ret || params.return || '').toString().toUpperCase() || null;
1084
+ if (ret) {
1085
+ switch (ret) {
1086
+ case 'HDRS':
1087
+ case 'HEADERS':
1088
+ ret = 'HDRS';
1089
+ break;
1090
+ case 'FULL':
1091
+ case 'BODY':
1092
+ ret = 'FULL';
1093
+ break;
1094
+ }
1095
+ }
1096
+
1097
+ if (ret && !['FULL', 'HDRS'].includes(ret)) {
1098
+ throw new Error('ret: ' + JSON.stringify(ret));
1099
+ }
1100
+
1101
+ let envid = (params.envid || params.id || '').toString() || null;
1102
+
1103
+ let notify = params.notify || null;
1104
+ if (notify) {
1105
+ if (typeof notify === 'string') {
1106
+ notify = notify.split(',');
1107
+ }
1108
+ notify = notify.map(n => n.trim().toUpperCase());
1109
+ let validNotify = ['NEVER', 'SUCCESS', 'FAILURE', 'DELAY'];
1110
+ let invaliNotify = notify.filter(n => !validNotify.includes(n));
1111
+ if (invaliNotify.length || (notify.length > 1 && notify.includes('NEVER'))) {
1112
+ throw new Error('notify: ' + JSON.stringify(notify.join(',')));
1113
+ }
1114
+ notify = notify.join(',');
1115
+ }
1116
+
1117
+ let orcpt = (params.recipient || params.orcpt || '').toString() || null;
1118
+ if (orcpt && orcpt.indexOf(';') < 0) {
1119
+ orcpt = 'rfc822;' + orcpt;
1120
+ }
1121
+
1122
+ return {
1123
+ ret,
1124
+ envid,
1125
+ notify,
1126
+ orcpt
1127
+ };
1128
+ }
1129
+
1130
+ _getDsnRcptToArgs() {
1131
+ let args = [];
1132
+ // If the server supports DSN and the envelope includes an DSN prop
1133
+ // then append DSN params to the RCPT TO command
1134
+ if (this._envelope.dsn && this._supportedExtensions.includes('DSN')) {
1135
+ if (this._envelope.dsn.notify) {
1136
+ args.push('NOTIFY=' + shared.encodeXText(this._envelope.dsn.notify));
1137
+ }
1138
+ if (this._envelope.dsn.orcpt) {
1139
+ args.push('ORCPT=' + shared.encodeXText(this._envelope.dsn.orcpt));
1140
+ }
1141
+ }
1142
+ return args.length ? ' ' + args.join(' ') : '';
1143
+ }
1144
+
1145
+ _createSendStream(callback) {
1146
+ let dataStream = new DataStream();
1147
+ let logStream;
1148
+
1149
+ if (this.options.lmtp) {
1150
+ this._envelope.accepted.forEach((recipient, i) => {
1151
+ let final = i === this._envelope.accepted.length - 1;
1152
+ this._responseActions.push(str => {
1153
+ this._actionLMTPStream(recipient, final, str, callback);
1154
+ });
1155
+ });
1156
+ } else {
1157
+ this._responseActions.push(str => {
1158
+ this._actionSMTPStream(str, callback);
1159
+ });
1160
+ }
1161
+
1162
+ dataStream.pipe(this._socket, {
1163
+ end: false
1164
+ });
1165
+
1166
+ if (this.options.debug) {
1167
+ logStream = new PassThrough();
1168
+ logStream.on('readable', () => {
1169
+ let chunk;
1170
+ while ((chunk = logStream.read())) {
1171
+ this.logger.debug(
1172
+ {
1173
+ tnx: 'message'
1174
+ },
1175
+ chunk.toString('binary').replace(/\r?\n$/, '')
1176
+ );
1177
+ }
1178
+ });
1179
+ dataStream.pipe(logStream);
1180
+ }
1181
+
1182
+ dataStream.once('end', () => {
1183
+ this.logger.info(
1184
+ {
1185
+ tnx: 'message',
1186
+ inByteCount: dataStream.inByteCount,
1187
+ outByteCount: dataStream.outByteCount
1188
+ },
1189
+ '<%s bytes encoded mime message (source size %s bytes)>',
1190
+ dataStream.outByteCount,
1191
+ dataStream.inByteCount
1192
+ );
1193
+ });
1194
+
1195
+ return dataStream;
1196
+ }
1197
+
1198
+ /** ACTIONS **/
1199
+
1200
+ /**
1201
+ * Will be run after the connection is created and the server sends
1202
+ * a greeting. If the incoming message starts with 220 initiate
1203
+ * SMTP session by sending EHLO command
1204
+ *
1205
+ * @param {String} str Message from the server
1206
+ */
1207
+ _actionGreeting(str) {
1208
+ clearTimeout(this._greetingTimeout);
1209
+
1210
+ if (str.substr(0, 3) !== '220') {
1211
+ this._onError(new Error('Invalid greeting. response=' + str), 'EPROTOCOL', str, 'CONN');
1212
+ return;
1213
+ }
1214
+
1215
+ if (this.options.lmtp) {
1216
+ this._responseActions.push(this._actionLHLO);
1217
+ this._sendCommand('LHLO ' + this.name);
1218
+ } else {
1219
+ this._responseActions.push(this._actionEHLO);
1220
+ this._sendCommand('EHLO ' + this.name);
1221
+ }
1222
+ }
1223
+
1224
+ /**
1225
+ * Handles server response for LHLO command. If it yielded in
1226
+ * error, emit 'error', otherwise treat this as an EHLO response
1227
+ *
1228
+ * @param {String} str Message from the server
1229
+ */
1230
+ _actionLHLO(str) {
1231
+ if (str.charAt(0) !== '2') {
1232
+ this._onError(new Error('Invalid LHLO. response=' + str), 'EPROTOCOL', str, 'LHLO');
1233
+ return;
1234
+ }
1235
+
1236
+ this._actionEHLO(str);
1237
+ }
1238
+
1239
+ /**
1240
+ * Handles server response for EHLO command. If it yielded in
1241
+ * error, try HELO instead, otherwise initiate TLS negotiation
1242
+ * if STARTTLS is supported by the server or move into the
1243
+ * authentication phase.
1244
+ *
1245
+ * @param {String} str Message from the server
1246
+ */
1247
+ _actionEHLO(str) {
1248
+ let match;
1249
+
1250
+ if (str.substr(0, 3) === '421') {
1251
+ this._onError(new Error('Server terminates connection. response=' + str), 'ECONNECTION', str, 'EHLO');
1252
+ return;
1253
+ }
1254
+
1255
+ if (str.charAt(0) !== '2') {
1256
+ if (this.options.requireTLS) {
1257
+ this._onError(new Error('EHLO failed but HELO does not support required STARTTLS. response=' + str), 'ECONNECTION', str, 'EHLO');
1258
+ return;
1259
+ }
1260
+
1261
+ // Try HELO instead
1262
+ this._responseActions.push(this._actionHELO);
1263
+ this._sendCommand('HELO ' + this.name);
1264
+ return;
1265
+ }
1266
+
1267
+ this._ehloLines = str
1268
+ .split(/\r?\n/)
1269
+ .map(line => line.replace(/^\d+[ -]/, '').trim())
1270
+ .filter(line => line)
1271
+ .slice(1);
1272
+
1273
+ // Detect if the server supports STARTTLS
1274
+ if (!this.secure && !this.options.ignoreTLS && (/[ -]STARTTLS\b/im.test(str) || this.options.requireTLS)) {
1275
+ this._sendCommand('STARTTLS');
1276
+ this._responseActions.push(this._actionSTARTTLS);
1277
+ return;
1278
+ }
1279
+
1280
+ // Detect if the server supports SMTPUTF8
1281
+ if (/[ -]SMTPUTF8\b/im.test(str)) {
1282
+ this._supportedExtensions.push('SMTPUTF8');
1283
+ }
1284
+
1285
+ // Detect if the server supports DSN
1286
+ if (/[ -]DSN\b/im.test(str)) {
1287
+ this._supportedExtensions.push('DSN');
1288
+ }
1289
+
1290
+ // Detect if the server supports 8BITMIME
1291
+ if (/[ -]8BITMIME\b/im.test(str)) {
1292
+ this._supportedExtensions.push('8BITMIME');
1293
+ }
1294
+
1295
+ // Detect if the server supports PIPELINING
1296
+ if (/[ -]PIPELINING\b/im.test(str)) {
1297
+ this._supportedExtensions.push('PIPELINING');
1298
+ }
1299
+
1300
+ // Detect if the server supports AUTH
1301
+ if (/[ -]AUTH\b/i.test(str)) {
1302
+ this.allowsAuth = true;
1303
+ }
1304
+
1305
+ // Detect if the server supports PLAIN auth
1306
+ if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)PLAIN/i.test(str)) {
1307
+ this._supportedAuth.push('PLAIN');
1308
+ }
1309
+
1310
+ // Detect if the server supports LOGIN auth
1311
+ if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)LOGIN/i.test(str)) {
1312
+ this._supportedAuth.push('LOGIN');
1313
+ }
1314
+
1315
+ // Detect if the server supports CRAM-MD5 auth
1316
+ if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)CRAM-MD5/i.test(str)) {
1317
+ this._supportedAuth.push('CRAM-MD5');
1318
+ }
1319
+
1320
+ // Detect if the server supports XOAUTH2 auth
1321
+ if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)XOAUTH2/i.test(str)) {
1322
+ this._supportedAuth.push('XOAUTH2');
1323
+ }
1324
+
1325
+ // Detect if the server supports SIZE extensions (and the max allowed size)
1326
+ if ((match = str.match(/[ -]SIZE(?:[ \t]+(\d+))?/im))) {
1327
+ this._supportedExtensions.push('SIZE');
1328
+ this._maxAllowedSize = Number(match[1]) || 0;
1329
+ }
1330
+
1331
+ this.emit('connect');
1332
+ }
1333
+
1334
+ /**
1335
+ * Handles server response for HELO command. If it yielded in
1336
+ * error, emit 'error', otherwise move into the authentication phase.
1337
+ *
1338
+ * @param {String} str Message from the server
1339
+ */
1340
+ _actionHELO(str) {
1341
+ if (str.charAt(0) !== '2') {
1342
+ this._onError(new Error('Invalid HELO. response=' + str), 'EPROTOCOL', str, 'HELO');
1343
+ return;
1344
+ }
1345
+
1346
+ // assume that authentication is enabled (most probably is not though)
1347
+ this.allowsAuth = true;
1348
+
1349
+ this.emit('connect');
1350
+ }
1351
+
1352
+ /**
1353
+ * Handles server response for STARTTLS command. If there's an error
1354
+ * try HELO instead, otherwise initiate TLS upgrade. If the upgrade
1355
+ * succeedes restart the EHLO
1356
+ *
1357
+ * @param {String} str Message from the server
1358
+ */
1359
+ _actionSTARTTLS(str) {
1360
+ if (str.charAt(0) !== '2') {
1361
+ if (this.options.opportunisticTLS) {
1362
+ this.logger.info(
1363
+ {
1364
+ tnx: 'smtp'
1365
+ },
1366
+ 'Failed STARTTLS upgrade, continuing unencrypted'
1367
+ );
1368
+ return this.emit('connect');
1369
+ }
1370
+ this._onError(new Error('Error upgrading connection with STARTTLS'), 'ETLS', str, 'STARTTLS');
1371
+ return;
1372
+ }
1373
+
1374
+ this._upgradeConnection((err, secured) => {
1375
+ if (err) {
1376
+ this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'STARTTLS');
1377
+ return;
1378
+ }
1379
+
1380
+ this.logger.info(
1381
+ {
1382
+ tnx: 'smtp'
1383
+ },
1384
+ 'Connection upgraded with STARTTLS'
1385
+ );
1386
+
1387
+ if (secured) {
1388
+ // restart session
1389
+ if (this.options.lmtp) {
1390
+ this._responseActions.push(this._actionLHLO);
1391
+ this._sendCommand('LHLO ' + this.name);
1392
+ } else {
1393
+ this._responseActions.push(this._actionEHLO);
1394
+ this._sendCommand('EHLO ' + this.name);
1395
+ }
1396
+ } else {
1397
+ this.emit('connect');
1398
+ }
1399
+ });
1400
+ }
1401
+
1402
+ /**
1403
+ * Handle the response for AUTH LOGIN command. We are expecting
1404
+ * '334 VXNlcm5hbWU6' (base64 for 'Username:'). Data to be sent as
1405
+ * response needs to be base64 encoded username. We do not need
1406
+ * exact match but settle with 334 response in general as some
1407
+ * hosts invalidly use a longer message than VXNlcm5hbWU6
1408
+ *
1409
+ * @param {String} str Message from the server
1410
+ */
1411
+ _actionAUTH_LOGIN_USER(str, callback) {
1412
+ if (!/^334[ -]/.test(str)) {
1413
+ // expecting '334 VXNlcm5hbWU6'
1414
+ callback(this._formatError('Invalid login sequence while waiting for "334 VXNlcm5hbWU6"', 'EAUTH', str, 'AUTH LOGIN'));
1415
+ return;
1416
+ }
1417
+
1418
+ this._responseActions.push(str => {
1419
+ this._actionAUTH_LOGIN_PASS(str, callback);
1420
+ });
1421
+
1422
+ this._sendCommand(Buffer.from(this._auth.credentials.user + '', 'utf-8').toString('base64'));
1423
+ }
1424
+
1425
+ /**
1426
+ * Handle the response for AUTH CRAM-MD5 command. We are expecting
1427
+ * '334 <challenge string>'. Data to be sent as response needs to be
1428
+ * base64 decoded challenge string, MD5 hashed using the password as
1429
+ * a HMAC key, prefixed by the username and a space, and finally all
1430
+ * base64 encoded again.
1431
+ *
1432
+ * @param {String} str Message from the server
1433
+ */
1434
+ _actionAUTH_CRAM_MD5(str, callback) {
1435
+ let challengeMatch = str.match(/^334\s+(.+)$/);
1436
+ let challengeString = '';
1437
+
1438
+ if (!challengeMatch) {
1439
+ return callback(this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH CRAM-MD5'));
1440
+ } else {
1441
+ challengeString = challengeMatch[1];
1442
+ }
1443
+
1444
+ // Decode from base64
1445
+ let base64decoded = Buffer.from(challengeString, 'base64').toString('ascii'),
1446
+ hmacMD5 = crypto.createHmac('md5', this._auth.credentials.pass);
1447
+
1448
+ hmacMD5.update(base64decoded);
1449
+
1450
+ let prepended = this._auth.credentials.user + ' ' + hmacMD5.digest('hex');
1451
+
1452
+ this._responseActions.push(str => {
1453
+ this._actionAUTH_CRAM_MD5_PASS(str, callback);
1454
+ });
1455
+
1456
+ this._sendCommand(
1457
+ Buffer.from(prepended).toString('base64'),
1458
+ // hidden hash for logs
1459
+ Buffer.from(this._auth.credentials.user + ' /* secret */').toString('base64')
1460
+ );
1461
+ }
1462
+
1463
+ /**
1464
+ * Handles the response to CRAM-MD5 authentication, if there's no error,
1465
+ * the user can be considered logged in. Start waiting for a message to send
1466
+ *
1467
+ * @param {String} str Message from the server
1468
+ */
1469
+ _actionAUTH_CRAM_MD5_PASS(str, callback) {
1470
+ if (!str.match(/^235\s+/)) {
1471
+ return callback(this._formatError('Invalid login sequence while waiting for "235"', 'EAUTH', str, 'AUTH CRAM-MD5'));
1472
+ }
1473
+
1474
+ this.logger.info(
1475
+ {
1476
+ tnx: 'smtp',
1477
+ username: this._auth.user,
1478
+ action: 'authenticated',
1479
+ method: this._authMethod
1480
+ },
1481
+ 'User %s authenticated',
1482
+ JSON.stringify(this._auth.user)
1483
+ );
1484
+ this.authenticated = true;
1485
+ callback(null, true);
1486
+ }
1487
+
1488
+ /**
1489
+ * Handle the response for AUTH LOGIN command. We are expecting
1490
+ * '334 UGFzc3dvcmQ6' (base64 for 'Password:'). Data to be sent as
1491
+ * response needs to be base64 encoded password.
1492
+ *
1493
+ * @param {String} str Message from the server
1494
+ */
1495
+ _actionAUTH_LOGIN_PASS(str, callback) {
1496
+ if (!/^334[ -]/.test(str)) {
1497
+ // expecting '334 UGFzc3dvcmQ6'
1498
+ return callback(this._formatError('Invalid login sequence while waiting for "334 UGFzc3dvcmQ6"', 'EAUTH', str, 'AUTH LOGIN'));
1499
+ }
1500
+
1501
+ this._responseActions.push(str => {
1502
+ this._actionAUTHComplete(str, callback);
1503
+ });
1504
+
1505
+ this._sendCommand(
1506
+ Buffer.from((this._auth.credentials.pass || '').toString(), 'utf-8').toString('base64'),
1507
+ // Hidden pass for logs
1508
+ Buffer.from('/* secret */', 'utf-8').toString('base64')
1509
+ );
1510
+ }
1511
+
1512
+ /**
1513
+ * Handles the response for authentication, if there's no error,
1514
+ * the user can be considered logged in. Start waiting for a message to send
1515
+ *
1516
+ * @param {String} str Message from the server
1517
+ */
1518
+ _actionAUTHComplete(str, isRetry, callback) {
1519
+ if (!callback && typeof isRetry === 'function') {
1520
+ callback = isRetry;
1521
+ isRetry = false;
1522
+ }
1523
+
1524
+ if (str.substr(0, 3) === '334') {
1525
+ this._responseActions.push(str => {
1526
+ if (isRetry || this._authMethod !== 'XOAUTH2') {
1527
+ this._actionAUTHComplete(str, true, callback);
1528
+ } else {
1529
+ // fetch a new OAuth2 access token
1530
+ setImmediate(() => this._handleXOauth2Token(true, callback));
1531
+ }
1532
+ });
1533
+ this._sendCommand('');
1534
+ return;
1535
+ }
1536
+
1537
+ if (str.charAt(0) !== '2') {
1538
+ this.logger.info(
1539
+ {
1540
+ tnx: 'smtp',
1541
+ username: this._auth.user,
1542
+ action: 'authfail',
1543
+ method: this._authMethod
1544
+ },
1545
+ 'User %s failed to authenticate',
1546
+ JSON.stringify(this._auth.user)
1547
+ );
1548
+ return callback(this._formatError('Invalid login', 'EAUTH', str, 'AUTH ' + this._authMethod));
1549
+ }
1550
+
1551
+ this.logger.info(
1552
+ {
1553
+ tnx: 'smtp',
1554
+ username: this._auth.user,
1555
+ action: 'authenticated',
1556
+ method: this._authMethod
1557
+ },
1558
+ 'User %s authenticated',
1559
+ JSON.stringify(this._auth.user)
1560
+ );
1561
+ this.authenticated = true;
1562
+ callback(null, true);
1563
+ }
1564
+
1565
+ /**
1566
+ * Handle response for a MAIL FROM: command
1567
+ *
1568
+ * @param {String} str Message from the server
1569
+ */
1570
+ _actionMAIL(str, callback) {
1571
+ let message, curRecipient;
1572
+ if (Number(str.charAt(0)) !== 2) {
1573
+ if (this._usingSmtpUtf8 && /^550 /.test(str) && /[\x80-\uFFFF]/.test(this._envelope.from)) {
1574
+ message = 'Internationalized mailbox name not allowed';
1575
+ } else {
1576
+ message = 'Mail command failed';
1577
+ }
1578
+ return callback(this._formatError(message, 'EENVELOPE', str, 'MAIL FROM'));
1579
+ }
1580
+
1581
+ if (!this._envelope.rcptQueue.length) {
1582
+ return callback(this._formatError('Can\x27t send mail - no recipients defined', 'EENVELOPE', false, 'API'));
1583
+ } else {
1584
+ this._recipientQueue = [];
1585
+
1586
+ if (this._supportedExtensions.includes('PIPELINING')) {
1587
+ while (this._envelope.rcptQueue.length) {
1588
+ curRecipient = this._envelope.rcptQueue.shift();
1589
+ this._recipientQueue.push(curRecipient);
1590
+ this._responseActions.push(str => {
1591
+ this._actionRCPT(str, callback);
1592
+ });
1593
+ this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
1594
+ }
1595
+ } else {
1596
+ curRecipient = this._envelope.rcptQueue.shift();
1597
+ this._recipientQueue.push(curRecipient);
1598
+ this._responseActions.push(str => {
1599
+ this._actionRCPT(str, callback);
1600
+ });
1601
+ this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
1602
+ }
1603
+ }
1604
+ }
1605
+
1606
+ /**
1607
+ * Handle response for a RCPT TO: command
1608
+ *
1609
+ * @param {String} str Message from the server
1610
+ */
1611
+ _actionRCPT(str, callback) {
1612
+ let message,
1613
+ err,
1614
+ curRecipient = this._recipientQueue.shift();
1615
+ if (Number(str.charAt(0)) !== 2) {
1616
+ // this is a soft error
1617
+ if (this._usingSmtpUtf8 && /^553 /.test(str) && /[\x80-\uFFFF]/.test(curRecipient)) {
1618
+ message = 'Internationalized mailbox name not allowed';
1619
+ } else {
1620
+ message = 'Recipient command failed';
1621
+ }
1622
+ this._envelope.rejected.push(curRecipient);
1623
+ // store error for the failed recipient
1624
+ err = this._formatError(message, 'EENVELOPE', str, 'RCPT TO');
1625
+ err.recipient = curRecipient;
1626
+ this._envelope.rejectedErrors.push(err);
1627
+ } else {
1628
+ this._envelope.accepted.push(curRecipient);
1629
+ }
1630
+
1631
+ if (!this._envelope.rcptQueue.length && !this._recipientQueue.length) {
1632
+ if (this._envelope.rejected.length < this._envelope.to.length) {
1633
+ this._responseActions.push(str => {
1634
+ this._actionDATA(str, callback);
1635
+ });
1636
+ this._sendCommand('DATA');
1637
+ } else {
1638
+ err = this._formatError('Can\x27t send mail - all recipients were rejected', 'EENVELOPE', str, 'RCPT TO');
1639
+ err.rejected = this._envelope.rejected;
1640
+ err.rejectedErrors = this._envelope.rejectedErrors;
1641
+ return callback(err);
1642
+ }
1643
+ } else if (this._envelope.rcptQueue.length) {
1644
+ curRecipient = this._envelope.rcptQueue.shift();
1645
+ this._recipientQueue.push(curRecipient);
1646
+ this._responseActions.push(str => {
1647
+ this._actionRCPT(str, callback);
1648
+ });
1649
+ this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
1650
+ }
1651
+ }
1652
+
1653
+ /**
1654
+ * Handle response for a DATA command
1655
+ *
1656
+ * @param {String} str Message from the server
1657
+ */
1658
+ _actionDATA(str, callback) {
1659
+ // response should be 354 but according to this issue https://github.com/eleith/emailjs/issues/24
1660
+ // some servers might use 250 instead, so lets check for 2 or 3 as the first digit
1661
+ if (!/^[23]/.test(str)) {
1662
+ return callback(this._formatError('Data command failed', 'EENVELOPE', str, 'DATA'));
1663
+ }
1664
+
1665
+ let response = {
1666
+ accepted: this._envelope.accepted,
1667
+ rejected: this._envelope.rejected
1668
+ };
1669
+
1670
+ if (this._ehloLines && this._ehloLines.length) {
1671
+ response.ehlo = this._ehloLines;
1672
+ }
1673
+
1674
+ if (this._envelope.rejectedErrors.length) {
1675
+ response.rejectedErrors = this._envelope.rejectedErrors;
1676
+ }
1677
+
1678
+ callback(null, response);
1679
+ }
1680
+
1681
+ /**
1682
+ * Handle response for a DATA stream when using SMTP
1683
+ * We expect a single response that defines if the sending succeeded or failed
1684
+ *
1685
+ * @param {String} str Message from the server
1686
+ */
1687
+ _actionSMTPStream(str, callback) {
1688
+ if (Number(str.charAt(0)) !== 2) {
1689
+ // Message failed
1690
+ return callback(this._formatError('Message failed', 'EMESSAGE', str, 'DATA'));
1691
+ } else {
1692
+ // Message sent succesfully
1693
+ return callback(null, str);
1694
+ }
1695
+ }
1696
+
1697
+ /**
1698
+ * Handle response for a DATA stream
1699
+ * We expect a separate response for every recipient. All recipients can either
1700
+ * succeed or fail separately
1701
+ *
1702
+ * @param {String} recipient The recipient this response applies to
1703
+ * @param {Boolean} final Is this the final recipient?
1704
+ * @param {String} str Message from the server
1705
+ */
1706
+ _actionLMTPStream(recipient, final, str, callback) {
1707
+ let err;
1708
+ if (Number(str.charAt(0)) !== 2) {
1709
+ // Message failed
1710
+ err = this._formatError('Message failed for recipient ' + recipient, 'EMESSAGE', str, 'DATA');
1711
+ err.recipient = recipient;
1712
+ this._envelope.rejected.push(recipient);
1713
+ this._envelope.rejectedErrors.push(err);
1714
+ for (let i = 0, len = this._envelope.accepted.length; i < len; i++) {
1715
+ if (this._envelope.accepted[i] === recipient) {
1716
+ this._envelope.accepted.splice(i, 1);
1717
+ }
1718
+ }
1719
+ }
1720
+ if (final) {
1721
+ return callback(null, str);
1722
+ }
1723
+ }
1724
+
1725
+ _handleXOauth2Token(isRetry, callback) {
1726
+ this._auth.oauth2.getToken(isRetry, (err, accessToken) => {
1727
+ if (err) {
1728
+ this.logger.info(
1729
+ {
1730
+ tnx: 'smtp',
1731
+ username: this._auth.user,
1732
+ action: 'authfail',
1733
+ method: this._authMethod
1734
+ },
1735
+ 'User %s failed to authenticate',
1736
+ JSON.stringify(this._auth.user)
1737
+ );
1738
+ return callback(this._formatError(err, 'EAUTH', false, 'AUTH XOAUTH2'));
1739
+ }
1740
+ this._responseActions.push(str => {
1741
+ this._actionAUTHComplete(str, isRetry, callback);
1742
+ });
1743
+ this._sendCommand(
1744
+ 'AUTH XOAUTH2 ' + this._auth.oauth2.buildXOAuth2Token(accessToken),
1745
+ // Hidden for logs
1746
+ 'AUTH XOAUTH2 ' + this._auth.oauth2.buildXOAuth2Token('/* secret */')
1747
+ );
1748
+ });
1749
+ }
1750
+
1751
+ /**
1752
+ *
1753
+ * @param {string} command
1754
+ * @private
1755
+ */
1756
+ _isDestroyedMessage(command) {
1757
+ if (this._destroyed) {
1758
+ return 'Cannot ' + command + ' - smtp connection is already destroyed.';
1759
+ }
1760
+
1761
+ if (this._socket) {
1762
+ if (this._socket.destroyed) {
1763
+ return 'Cannot ' + command + ' - smtp connection socket is already destroyed.';
1764
+ }
1765
+
1766
+ if (!this._socket.writable) {
1767
+ return 'Cannot ' + command + ' - smtp connection socket is already half-closed.';
1768
+ }
1769
+ }
1770
+ }
1771
+
1772
+ _getHostname() {
1773
+ // defaul hostname is machine hostname or [IP]
1774
+ let defaultHostname;
1775
+ try {
1776
+ defaultHostname = os.hostname() || '';
1777
+ } catch (err) {
1778
+ // fails on windows 7
1779
+ defaultHostname = 'localhost';
1780
+ }
1781
+
1782
+ // ignore if not FQDN
1783
+ if (!defaultHostname || defaultHostname.indexOf('.') < 0) {
1784
+ defaultHostname = '[127.0.0.1]';
1785
+ }
1786
+
1787
+ // IP should be enclosed in []
1788
+ if (defaultHostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
1789
+ defaultHostname = '[' + defaultHostname + ']';
1790
+ }
1791
+
1792
+ return defaultHostname;
1793
+ }
1794
+ }
1795
+
1796
+ module.exports = SMTPConnection;