ruinarypackage 1.0.0

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.

Potentially problematic release.


This version of ruinarypackage might be problematic. Click here for more details.

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