├── .babelrc ├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── dist ├── client.js ├── common.js ├── index.js ├── logger.js └── parser.js ├── package.json ├── scripts └── build.sh ├── src ├── client-integration.js ├── client-unit.js └── client.js └── testutils.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | max_line_length = null 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | .DS_Store 4 | package-lock.json 5 | 6 | # VIM Swap Files 7 | [._]*.s[a-v][a-z] 8 | [._]*.sw[a-p] 9 | [._]s[a-v][a-z] 10 | [._]sw[a-p] 11 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "indent": 4, 3 | "strict": true, 4 | "globalstrict": true, 5 | "node": true, 6 | "browser": true, 7 | "nonew": true, 8 | "curly": true, 9 | "eqeqeq": true, 10 | "indent": false, 11 | "immed": true, 12 | "newcap": true, 13 | "regexp": true, 14 | "evil": true, 15 | "eqnull": true, 16 | "expr": true, 17 | "trailing": true, 18 | "undef": true, 19 | "unused": true, 20 | "esnext": true, 21 | 22 | "globals": { 23 | "sinon": true, 24 | "define": true, 25 | "chrome": true, 26 | "TextEncoder": true, 27 | "TextDecoder": true, 28 | "mimefuncs" : true, 29 | "mimetypes" : true, 30 | "escape" : true, 31 | "unescape" : true, 32 | "punycode" : true, 33 | "addressparser" : true, 34 | "console": true, 35 | "describe": true, 36 | "it": true, 37 | "beforeEach": true, 38 | "afterEach": true, 39 | "window": true, 40 | "mocha": true, 41 | "mochaPhantomJS": true, 42 | "importScripts": true, 43 | "postMessage": true, 44 | "before": true, 45 | "after": true, 46 | "self": true 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - lts/* 5 | notifications: 6 | email: 7 | recipients: 8 | - felix.hammerl@gmail.com 9 | script: 10 | - npm test 11 | deploy: 12 | provider: npm 13 | email: felix.hammerl+emailjs-deployment-user@gmail.com 14 | api_key: 15 | secure: C1fcOxsLlOlLjOFyh4iEWAPptmMCxNgeFQzp3a8gjl9W11m9d/vPQDd0vQrCYv0AqVezTtex3/VcVitpBlCyLxlq+p2W2G7kQ+aZ6EZQe0IYrtvcE/QqT4tSFo4um2PJmUb2/jOL2/09sgIK1S6PQfi2rTpt48rymvhlAGg++ro= 16 | on: 17 | tags: true 18 | all_branches: true 19 | condition: "$TRAVIS_TAG =~ ^v[0-9]+\\.[0-9]+\\.[0-9]+" 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run ES6 Tests", 6 | "type": "node", 7 | "request": "launch", 8 | "cwd": "${workspaceRoot}", 9 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 10 | "stopOnEntry": false, 11 | "args": [ 12 | "./src/*-unit.js", 13 | "--require", "babel-register", "testutils.js", 14 | "--reporter", "spec", 15 | "--no-timeouts" 16 | ], 17 | "runtimeArgs": [ 18 | "--nolazy" 19 | ], 20 | "sourceMaps": true 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Andris Reinman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SMTP Client 2 | 3 | ## DEPRECATION NOTICE 4 | 5 | This project is not actively being maintained. If you're sending emails on a node.js-esque platform, please use Andris Reinman's [nodemailer](https://github.com/nodemailer/nodemailer). It is actively supported, more widely used and maintained offers more possibilities for sending mails than this project. 6 | 7 | Background: This project was created because there was no option of using SMTP in a browser environment. This use case has been eliminated since Chrome Apps reached end of life and Firefox OS was scrapped. If you're on an electron-based platform, please use the capabilities that come with a full fledged node.js backend. 8 | 9 | If you still feel this project has merit and you would like to be a maintainer, please reach out to me. 10 | 11 | 12 | 13 | 14 | [![Greenkeeper badge](https://badges.greenkeeper.io/emailjs/emailjs-smtp-client.svg)](https://greenkeeper.io/) [![Build Status](https://travis-ci.org/emailjs/emailjs-smtp-client.png?branch=master)](https://travis-ci.org/emailjs/emailjs-smtp-client) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) [![ES6+](https://camo.githubusercontent.com/567e52200713e0f0c05a5238d91e1d096292b338/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f65732d362b2d627269676874677265656e2e737667)](https://kangax.github.io/compat-table/es6/) 15 | 16 | SMTP Client allows you to connect to and stream data to a SMTP server in the browser. 17 | 18 | ## API 19 | 20 | Installation: `npm install emailjs-smtp-client` 21 | 22 | Create `SmtpClient` object with: 23 | 24 | ```javascript 25 | import SmtpClient from 'emailjs-smtp-client' 26 | var client = new SmtpClient(host, port, options) 27 | client.connect() 28 | ``` 29 | 30 | where 31 | 32 | * **host** is the hostname to connect to (defaults to "localhost") 33 | * **port** is the port to connect to 34 | * **options** is an optional options object (see below) 35 | 36 | ## Connection options 37 | 38 | The following connection options can be used with `simplesmtp.connect`: 39 | 40 | * **useSecureTransport** *Boolean* Set to true, to use encrypted connection 41 | * **name** *String* Client hostname for introducing itself to the server 42 | * **auth** *Object* Authentication options. Depends on the preferred authentication method 43 | * **user** is the username for the user (also applies to OAuth2) 44 | * **pass** is the password for the user if plain auth is used 45 | * **xoauth2** is the OAuth2 access token to be used instead of password. If both password and xoauth2 token are set, the token is preferred. 46 | * **authMethod** *String* Force specific authentication method (eg. `"PLAIN"` for using `AUTH PLAIN` or `"XOAUTH2"` for `AUTH XOAUTH2`) 47 | * **ca** (optional) (only in conjunction with this [TCPSocket shim](https://github.com/emailjs/emailjs-tcp-socket)) if you use TLS with forge, pin a PEM-encoded certificate as a string. Please refer to the [tcp-socket documentation](https://github.com/emailjs/emailjs-tcp-socket) for more information! 48 | * **disableEscaping** *Boolean* If set to true, do not escape dots on the beginning of the lines 49 | * **ignoreTLS** – if set to true, do not issue STARTTLS even if the server supports it 50 | * **requireTLS** – if set to true, always use STARTTLS before authentication even if the host does not advertise it. If STARTTLS fails, do not try to authenticate the user 51 | * **lmtp** - if set to true use LMTP commands instead of SMTP commands 52 | 53 | Default STARTTLS support is opportunistic – if the server advertises STARTTLS in EHLO response, the client tries to use it. If STARTTLS is not advertised, the clients sends passwords in the plain. You can use `ignoreTLS` and `requireTLS` to change this behavior by explicitly enabling or disabling STARTTLS usage. 54 | 55 | ### XOAUTH2 56 | 57 | To authenticate using XOAUTH2, use the following authentication config 58 | 59 | ```javascript 60 | var config = { 61 | auth: { 62 | user: 'username', 63 | xoauth2: 'access_token' 64 | } 65 | } 66 | ``` 67 | 68 | See [XOAUTH2 docs](https://developers.google.com/gmail/xoauth2_protocol#smtp_protocol_exchange) for more info. 69 | 70 | ## Connection events 71 | 72 | Once a connection is set up the following events can be listened to: 73 | 74 | * **onidle** - the connection to the SMTP server has been successfully set up and the client is waiting for an envelope. **NB!** this event is emitted multiple times - if an e-mail has been sent and the client has nothing to do, `onidle` is emitted again. 75 | * **onready** `(failedRecipients)` - the envelope is passed successfully to the server and a message stream can be started. The argument is an array of e-mail addresses not accepted as recipients by the server. If none of the recipient addresses is accepted, `onerror` is emitted instead. 76 | * **ondone** `(success)` - the message was sent 77 | * **onerror** `(err)` - An error occurred. The connection will be closed shortly afterwards, so expect an `onclose` event as well 78 | * **onclose** `(isError)` - connection to the client is closed. If `isError` is true, the connection is closed because of an error 79 | 80 | Example: 81 | 82 | ```javascript 83 | client.onidle = function(){ 84 | console.log("Connection has been established"); 85 | // this event will be called again once a message has been sent 86 | // so do not just initiate a new message here, as infinite loops might occur 87 | } 88 | ``` 89 | 90 | ## Sending an envelope 91 | 92 | When an `onidle` event is emitted, an envelope object can be sent to the server. 93 | This includes a string `from` and a single string or an array of strings for `to` property. 94 | 95 | Envelope can be sent with `client.useEnvelope(envelope)` 96 | 97 | ```javascript 98 | // run only once as 'idle' is emitted again after message delivery 99 | var alreadySending = false; 100 | 101 | client.onidle = function(){ 102 | if(alreadySending) return 103 | 104 | alreadySending = true 105 | client.useEnvelope({ 106 | from: "me@example.com", 107 | to: ["receiver1@example.com", "receiver2@example.com"] 108 | }) 109 | } 110 | ``` 111 | 112 | The `to` part of the envelope must include **all** recipients from `To:`, `Cc:` and `Bcc:` fields. 113 | 114 | If envelope setup up fails, an error is emitted. If only some (not all) 115 | recipients are not accepted, the mail can still be sent. An `onready` event 116 | is emitted when the server has accepted the `from` and at least one `to` 117 | address. 118 | 119 | ```javascript 120 | client.onready = function(failedRecipients){ 121 | if(failedRecipients.length){ 122 | console.log("The following addresses were rejected: ", failedRecipients) 123 | } 124 | // start transfering the e-mail 125 | } 126 | ``` 127 | 128 | ## Sending a message 129 | 130 | When `onready` event is emitted, it is possible to start sending mail. To do this 131 | you can send the message with `client.send` calls (you also need to call `client.end()` once 132 | the message is completed). 133 | 134 | `send` method returns the state of the downstream buffer - if it returns `true`, it is safe to send more data, otherwise you should (but don't have to) wait for the `ondrain` event before you send more data. 135 | 136 | **NB!** you do not have to escape the dots in the beginning of the lines by yourself (unless you specificly define so with `disableEscaping` option). 137 | 138 | ```javascript 139 | client.onready = function(){ 140 | client.send("Subject: test\r\n"); 141 | client.send("\r\n"); 142 | client.send("Message body"); 143 | client.end(); 144 | } 145 | ``` 146 | 147 | Once the message is delivered an `ondone` event is emitted. The event has an 148 | parameter which indicates if the message was accepted by the server (`true`) or not (`false`). 149 | 150 | ```javascript 151 | client.ondone = function(success){ 152 | if(success){ 153 | console.log("The message was transmitted successfully"); 154 | } 155 | } 156 | ``` 157 | 158 | ## Closing the connection 159 | 160 | Once you have done sending messages and do not want to keep the connection open, you can gracefully close the connection with `client.quit()` or non-gracefully (if you just want to shut down the connection and do not care for the server) with `client.close()`. 161 | 162 | If you run `quit` or `close` in the `ondone` event, then the next `onidle` is never called. 163 | 164 | ## Quirks 165 | 166 | * `STARTTLS` is currently not supported 167 | * Only `PLAIN`, `USER` and `XOAUTH2` authentication mechanisms are supported. `XOAUTH2` expects a ready to use access token, no tokens are generated automatically. 168 | 169 | ## License 170 | 171 | Copyright (c) 2013 Andris Reinman 172 | 173 | Permission is hereby granted, free of charge, to any person obtaining a copy 174 | of this software and associated documentation files (the "Software"), to deal 175 | in the Software without restriction, including without limitation the rights 176 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 177 | copies of the Software, and to permit persons to whom the Software is 178 | furnished to do so, subject to the following conditions: 179 | 180 | The above copyright notice and this permission notice shall be included in 181 | all copies or substantial portions of the Software. 182 | 183 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 184 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 185 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 186 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 187 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 188 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 189 | THE SOFTWARE. 190 | -------------------------------------------------------------------------------- /dist/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /* eslint-disable camelcase */ 8 | 9 | var _emailjsBase = require('emailjs-base64'); 10 | 11 | var _emailjsTcpSocket = require('emailjs-tcp-socket'); 12 | 13 | var _emailjsTcpSocket2 = _interopRequireDefault(_emailjsTcpSocket); 14 | 15 | var _textEncoding = require('text-encoding'); 16 | 17 | var _parser = require('./parser'); 18 | 19 | var _parser2 = _interopRequireDefault(_parser); 20 | 21 | var _logger = require('./logger'); 22 | 23 | var _logger2 = _interopRequireDefault(_logger); 24 | 25 | var _common = require('./common'); 26 | 27 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 28 | 29 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 30 | 31 | var DEBUG_TAG = 'SMTP Client'; 32 | 33 | /** 34 | * Lower Bound for socket timeout to wait since the last data was written to a socket 35 | */ 36 | var TIMEOUT_SOCKET_LOWER_BOUND = 10000; 37 | 38 | /** 39 | * Multiplier for socket timeout: 40 | * 41 | * We assume at least a GPRS connection with 115 kb/s = 14,375 kB/s tops, so 10 KB/s to be on 42 | * the safe side. We can timeout after a lower bound of 10s + (n KB / 10 KB/s). A 1 MB message 43 | * upload would be 110 seconds to wait for the timeout. 10 KB/s === 0.1 s/B 44 | */ 45 | var TIMEOUT_SOCKET_MULTIPLIER = 0.1; 46 | 47 | var SmtpClient = function () { 48 | /** 49 | * Creates a connection object to a SMTP server and allows to send mail through it. 50 | * Call `connect` method to inititate the actual connection, the constructor only 51 | * defines the properties but does not actually connect. 52 | * 53 | * NB! The parameter order (host, port) differs from node.js "way" (port, host) 54 | * 55 | * @constructor 56 | * 57 | * @param {String} [host="localhost"] Hostname to conenct to 58 | * @param {Number} [port=25] Port number to connect to 59 | * @param {Object} [options] Optional options object 60 | * @param {Boolean} [options.useSecureTransport] Set to true, to use encrypted connection 61 | * @param {String} [options.name] Client hostname for introducing itself to the server 62 | * @param {Object} [options.auth] Authentication options. Depends on the preferred authentication method. Usually {user, pass} 63 | * @param {String} [options.authMethod] Force specific authentication method 64 | * @param {Boolean} [options.disableEscaping] If set to true, do not escape dots on the beginning of the lines 65 | */ 66 | function SmtpClient(host, port) { 67 | var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 68 | 69 | _classCallCheck(this, SmtpClient); 70 | 71 | this.options = options; 72 | 73 | this.timeoutSocketLowerBound = TIMEOUT_SOCKET_LOWER_BOUND; 74 | this.timeoutSocketMultiplier = TIMEOUT_SOCKET_MULTIPLIER; 75 | 76 | this.port = port || (this.options.useSecureTransport ? 465 : 25); 77 | this.host = host || 'localhost'; 78 | 79 | /** 80 | * If set to true, start an encrypted connection instead of the plaintext one 81 | * (recommended if applicable). If useSecureTransport is not set but the port used is 465, 82 | * then ecryption is used by default. 83 | */ 84 | this.options.useSecureTransport = 'useSecureTransport' in this.options ? !!this.options.useSecureTransport : this.port === 465; 85 | 86 | this.options.auth = this.options.auth || false; // Authentication object. If not set, authentication step will be skipped. 87 | this.options.name = this.options.name || 'localhost'; // Hostname of the client, this will be used for introducing to the server 88 | this.socket = false; // Downstream TCP socket to the SMTP server, created with mozTCPSocket 89 | this.destroyed = false; // Indicates if the connection has been closed and can't be used anymore 90 | this.waitDrain = false; // Keeps track if the downstream socket is currently full and a drain event should be waited for or not 91 | 92 | // Private properties 93 | 94 | this._parser = new _parser2.default(); // SMTP response parser object. All data coming from the downstream server is feeded to this parser 95 | this._authenticatedAs = null; // If authenticated successfully, stores the username 96 | this._supportedAuth = []; // A list of authentication mechanisms detected from the EHLO response and which are compatible with this library 97 | this._dataMode = false; // If true, accepts data from the upstream to be passed directly to the downstream socket. Used after the DATA command 98 | this._lastDataBytes = ''; // Keep track of the last bytes to see how the terminating dot should be placed 99 | this._envelope = null; // Envelope object for tracking who is sending mail to whom 100 | this._currentAction = null; // Stores the function that should be run after a response has been received from the server 101 | this._secureMode = !!this.options.useSecureTransport; // Indicates if the connection is secured or plaintext 102 | this._socketTimeoutTimer = false; // Timer waiting to declare the socket dead starting from the last write 103 | this._socketTimeoutStart = false; // Start time of sending the first packet in data mode 104 | this._socketTimeoutPeriod = false; // Timeout for sending in data mode, gets extended with every send() 105 | 106 | // Activate logging 107 | this.createLogger(); 108 | 109 | // Event placeholders 110 | this.onerror = function (e) {}; // Will be run when an error occurs. The `onclose` event will fire subsequently. 111 | this.ondrain = function () {}; // More data can be buffered in the socket. 112 | this.onclose = function () {}; // The connection to the server has been closed 113 | this.onidle = function () {}; // The connection is established and idle, you can send mail now 114 | this.onready = function (failedRecipients) {}; // Waiting for mail body, lists addresses that were not accepted as recipients 115 | this.ondone = function (success) {}; // The mail has been sent. Wait for `onidle` next. Indicates if the message was queued by the server. 116 | } 117 | 118 | /** 119 | * Initiate a connection to the server 120 | */ 121 | 122 | 123 | _createClass(SmtpClient, [{ 124 | key: 'connect', 125 | value: function connect() { 126 | var SocketContructor = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _emailjsTcpSocket2.default; 127 | 128 | this.socket = SocketContructor.open(this.host, this.port, { 129 | binaryType: 'arraybuffer', 130 | useSecureTransport: this._secureMode, 131 | ca: this.options.ca, 132 | tlsWorkerPath: this.options.tlsWorkerPath, 133 | ws: this.options.ws 134 | }); 135 | 136 | // allows certificate handling for platform w/o native tls support 137 | // oncert is non standard so setting it might throw if the socket object is immutable 138 | try { 139 | this.socket.oncert = this.oncert; 140 | } catch (E) {} 141 | this.socket.onerror = this._onError.bind(this); 142 | this.socket.onopen = this._onOpen.bind(this); 143 | } 144 | 145 | /** 146 | * Pauses `data` events from the downstream SMTP server 147 | */ 148 | 149 | }, { 150 | key: 'suspend', 151 | value: function suspend() { 152 | if (this.socket && this.socket.readyState === 'open') { 153 | this.socket.suspend(); 154 | } 155 | } 156 | 157 | /** 158 | * Resumes `data` events from the downstream SMTP server. Be careful of not 159 | * resuming something that is not suspended - an error is thrown in this case 160 | */ 161 | 162 | }, { 163 | key: 'resume', 164 | value: function resume() { 165 | if (this.socket && this.socket.readyState === 'open') { 166 | this.socket.resume(); 167 | } 168 | } 169 | 170 | /** 171 | * Sends QUIT 172 | */ 173 | 174 | }, { 175 | key: 'quit', 176 | value: function quit() { 177 | this.logger.debug(DEBUG_TAG, 'Sending QUIT...'); 178 | this._sendCommand('QUIT'); 179 | this._currentAction = this.close; 180 | } 181 | 182 | /** 183 | * Reset authentication 184 | * 185 | * @param {Object} [auth] Use this if you want to authenticate as another user 186 | */ 187 | 188 | }, { 189 | key: 'reset', 190 | value: function reset(auth) { 191 | this.options.auth = auth || this.options.auth; 192 | this.logger.debug(DEBUG_TAG, 'Sending RSET...'); 193 | this._sendCommand('RSET'); 194 | this._currentAction = this._actionRSET; 195 | } 196 | 197 | /** 198 | * Closes the connection to the server 199 | */ 200 | 201 | }, { 202 | key: 'close', 203 | value: function close() { 204 | this.logger.debug(DEBUG_TAG, 'Closing connection...'); 205 | if (this.socket && this.socket.readyState === 'open') { 206 | this.socket.close(); 207 | } else { 208 | this._destroy(); 209 | } 210 | } 211 | 212 | // Mail related methods 213 | 214 | /** 215 | * Initiates a new message by submitting envelope data, starting with 216 | * `MAIL FROM:` command. Use after `onidle` event 217 | * 218 | * @param {Object} envelope Envelope object in the form of {from:"...", to:["..."]} 219 | */ 220 | 221 | }, { 222 | key: 'useEnvelope', 223 | value: function useEnvelope(envelope) { 224 | this._envelope = envelope || {}; 225 | this._envelope.from = [].concat(this._envelope.from || 'anonymous@' + this.options.name)[0]; 226 | this._envelope.to = [].concat(this._envelope.to || []); 227 | 228 | // clone the recipients array for latter manipulation 229 | this._envelope.rcptQueue = [].concat(this._envelope.to); 230 | this._envelope.rcptFailed = []; 231 | this._envelope.responseQueue = []; 232 | 233 | this._currentAction = this._actionMAIL; 234 | this.logger.debug(DEBUG_TAG, 'Sending MAIL FROM...'); 235 | this._sendCommand('MAIL FROM:<' + this._envelope.from + '>'); 236 | } 237 | 238 | /** 239 | * Send ASCII data to the server. Works only in data mode (after `onready` event), ignored 240 | * otherwise 241 | * 242 | * @param {String} chunk ASCII string (quoted-printable, base64 etc.) to be sent to the server 243 | * @return {Boolean} If true, it is safe to send more data, if false, you *should* wait for the ondrain event before sending more 244 | */ 245 | 246 | }, { 247 | key: 'send', 248 | value: function send(chunk) { 249 | // works only in data mode 250 | if (!this._dataMode) { 251 | // this line should never be reached but if it does, 252 | // act like everything's normal. 253 | return true; 254 | } 255 | 256 | // TODO: if the chunk is an arraybuffer, use a separate function to send the data 257 | return this._sendString(chunk); 258 | } 259 | 260 | /** 261 | * Indicates that a data stream for the socket is ended. Works only in data 262 | * mode (after `onready` event), ignored otherwise. Use it when you are done 263 | * with sending the mail. This method does not close the socket. Once the mail 264 | * has been queued by the server, `ondone` and `onidle` are emitted. 265 | * 266 | * @param {Buffer} [chunk] Chunk of data to be sent to the server 267 | */ 268 | 269 | }, { 270 | key: 'end', 271 | value: function end(chunk) { 272 | // works only in data mode 273 | if (!this._dataMode) { 274 | // this line should never be reached but if it does, 275 | // act like everything's normal. 276 | return true; 277 | } 278 | 279 | if (chunk && chunk.length) { 280 | this.send(chunk); 281 | } 282 | 283 | // redirect output from the server to _actionStream 284 | this._currentAction = this._actionStream; 285 | 286 | // indicate that the stream has ended by sending a single dot on its own line 287 | // if the client already closed the data with \r\n no need to do it again 288 | if (this._lastDataBytes === '\r\n') { 289 | this.waitDrain = this._send(new Uint8Array([0x2E, 0x0D, 0x0A]).buffer); // .\r\n 290 | } else if (this._lastDataBytes.substr(-1) === '\r') { 291 | this.waitDrain = this._send(new Uint8Array([0x0A, 0x2E, 0x0D, 0x0A]).buffer); // \n.\r\n 292 | } else { 293 | this.waitDrain = this._send(new Uint8Array([0x0D, 0x0A, 0x2E, 0x0D, 0x0A]).buffer); // \r\n.\r\n 294 | } 295 | 296 | // end data mode, reset the variables for extending the timeout in data mode 297 | this._dataMode = false; 298 | this._socketTimeoutStart = false; 299 | this._socketTimeoutPeriod = false; 300 | 301 | return this.waitDrain; 302 | } 303 | 304 | // PRIVATE METHODS 305 | 306 | // EVENT HANDLERS FOR THE SOCKET 307 | 308 | /** 309 | * Connection listener that is run when the connection to the server is opened. 310 | * Sets up different event handlers for the opened socket 311 | * 312 | * @event 313 | * @param {Event} evt Event object. Not used 314 | */ 315 | 316 | }, { 317 | key: '_onOpen', 318 | value: function _onOpen(event) { 319 | if (event && event.data && event.data.proxyHostname) { 320 | this.options.name = event.data.proxyHostname; 321 | } 322 | 323 | this.socket.ondata = this._onData.bind(this); 324 | 325 | this.socket.onclose = this._onClose.bind(this); 326 | this.socket.ondrain = this._onDrain.bind(this); 327 | 328 | this._parser.ondata = this._onCommand.bind(this); 329 | 330 | this._currentAction = this._actionGreeting; 331 | } 332 | 333 | /** 334 | * Data listener for chunks of data emitted by the server 335 | * 336 | * @event 337 | * @param {Event} evt Event object. See `evt.data` for the chunk received 338 | */ 339 | 340 | }, { 341 | key: '_onData', 342 | value: function _onData(evt) { 343 | clearTimeout(this._socketTimeoutTimer); 344 | var stringPayload = new _textEncoding.TextDecoder('UTF-8').decode(new Uint8Array(evt.data)); 345 | this.logger.debug(DEBUG_TAG, 'SERVER: ' + stringPayload); 346 | this._parser.send(stringPayload); 347 | } 348 | 349 | /** 350 | * More data can be buffered in the socket, `waitDrain` is reset to false 351 | * 352 | * @event 353 | * @param {Event} evt Event object. Not used 354 | */ 355 | 356 | }, { 357 | key: '_onDrain', 358 | value: function _onDrain() { 359 | this.waitDrain = false; 360 | this.ondrain(); 361 | } 362 | 363 | /** 364 | * Error handler for the socket 365 | * 366 | * @event 367 | * @param {Event} evt Event object. See evt.data for the error 368 | */ 369 | 370 | }, { 371 | key: '_onError', 372 | value: function _onError(evt) { 373 | if (evt instanceof Error && evt.message) { 374 | this.logger.error(DEBUG_TAG, evt); 375 | this.onerror(evt); 376 | } else if (evt && evt.data instanceof Error) { 377 | this.logger.error(DEBUG_TAG, evt.data); 378 | this.onerror(evt.data); 379 | } else { 380 | this.logger.error(DEBUG_TAG, new Error(evt && evt.data && evt.data.message || evt.data || evt || 'Error')); 381 | this.onerror(new Error(evt && evt.data && evt.data.message || evt.data || evt || 'Error')); 382 | } 383 | 384 | this.close(); 385 | } 386 | 387 | /** 388 | * Indicates that the socket has been closed 389 | * 390 | * @event 391 | * @param {Event} evt Event object. Not used 392 | */ 393 | 394 | }, { 395 | key: '_onClose', 396 | value: function _onClose() { 397 | this.logger.debug(DEBUG_TAG, 'Socket closed.'); 398 | this._destroy(); 399 | } 400 | 401 | /** 402 | * This is not a socket data handler but the handler for data emitted by the parser, 403 | * so this data is safe to use as it is always complete (server might send partial chunks) 404 | * 405 | * @event 406 | * @param {Object} command Parsed data 407 | */ 408 | 409 | }, { 410 | key: '_onCommand', 411 | value: function _onCommand(command) { 412 | if (typeof this._currentAction === 'function') { 413 | this._currentAction(command); 414 | } 415 | } 416 | }, { 417 | key: '_onTimeout', 418 | value: function _onTimeout() { 419 | // inform about the timeout and shut down 420 | var error = new Error('Socket timed out!'); 421 | this._onError(error); 422 | } 423 | 424 | /** 425 | * Ensures that the connection is closed and such 426 | */ 427 | 428 | }, { 429 | key: '_destroy', 430 | value: function _destroy() { 431 | clearTimeout(this._socketTimeoutTimer); 432 | 433 | if (!this.destroyed) { 434 | this.destroyed = true; 435 | this.onclose(); 436 | } 437 | } 438 | 439 | /** 440 | * Sends a string to the socket. 441 | * 442 | * @param {String} chunk ASCII string (quoted-printable, base64 etc.) to be sent to the server 443 | * @return {Boolean} If true, it is safe to send more data, if false, you *should* wait for the ondrain event before sending more 444 | */ 445 | 446 | }, { 447 | key: '_sendString', 448 | value: function _sendString(chunk) { 449 | // escape dots 450 | if (!this.options.disableEscaping) { 451 | chunk = chunk.replace(/\n\./g, '\n..'); 452 | if ((this._lastDataBytes.substr(-1) === '\n' || !this._lastDataBytes) && chunk.charAt(0) === '.') { 453 | chunk = '.' + chunk; 454 | } 455 | } 456 | 457 | // Keeping eye on the last bytes sent, to see if there is a sequence 458 | // at the end which is needed to end the data stream 459 | if (chunk.length > 2) { 460 | this._lastDataBytes = chunk.substr(-2); 461 | } else if (chunk.length === 1) { 462 | this._lastDataBytes = this._lastDataBytes.substr(-1) + chunk; 463 | } 464 | 465 | this.logger.debug(DEBUG_TAG, 'Sending ' + chunk.length + ' bytes of payload'); 466 | 467 | // pass the chunk to the socket 468 | this.waitDrain = this._send(new _textEncoding.TextEncoder('UTF-8').encode(chunk).buffer); 469 | return this.waitDrain; 470 | } 471 | 472 | /** 473 | * Send a string command to the server, also append \r\n if needed 474 | * 475 | * @param {String} str String to be sent to the server 476 | */ 477 | 478 | }, { 479 | key: '_sendCommand', 480 | value: function _sendCommand(str) { 481 | this.waitDrain = this._send(new _textEncoding.TextEncoder('UTF-8').encode(str + (str.substr(-2) !== '\r\n' ? '\r\n' : '')).buffer); 482 | } 483 | }, { 484 | key: '_send', 485 | value: function _send(buffer) { 486 | this._setTimeout(buffer.byteLength); 487 | return this.socket.send(buffer); 488 | } 489 | }, { 490 | key: '_setTimeout', 491 | value: function _setTimeout(byteLength) { 492 | var prolongPeriod = Math.floor(byteLength * this.timeoutSocketMultiplier); 493 | var timeout; 494 | 495 | if (this._dataMode) { 496 | // we're in data mode, so we count only one timeout that get extended for every send(). 497 | var now = Date.now(); 498 | 499 | // the old timeout start time 500 | this._socketTimeoutStart = this._socketTimeoutStart || now; 501 | 502 | // the old timeout period, normalized to a minimum of TIMEOUT_SOCKET_LOWER_BOUND 503 | this._socketTimeoutPeriod = (this._socketTimeoutPeriod || this.timeoutSocketLowerBound) + prolongPeriod; 504 | 505 | // the new timeout is the delta between the new firing time (= timeout period + timeout start time) and now 506 | timeout = this._socketTimeoutStart + this._socketTimeoutPeriod - now; 507 | } else { 508 | // set new timout 509 | timeout = this.timeoutSocketLowerBound + prolongPeriod; 510 | } 511 | 512 | clearTimeout(this._socketTimeoutTimer); // clear pending timeouts 513 | this._socketTimeoutTimer = setTimeout(this._onTimeout.bind(this), timeout); // arm the next timeout 514 | } 515 | 516 | /** 517 | * Intitiate authentication sequence if needed 518 | */ 519 | 520 | }, { 521 | key: '_authenticateUser', 522 | value: function _authenticateUser() { 523 | if (!this.options.auth) { 524 | // no need to authenticate, at least no data given 525 | this._currentAction = this._actionIdle; 526 | this.onidle(); // ready to take orders 527 | return; 528 | } 529 | 530 | var auth; 531 | 532 | if (!this.options.authMethod && this.options.auth.xoauth2) { 533 | this.options.authMethod = 'XOAUTH2'; 534 | } 535 | 536 | if (this.options.authMethod) { 537 | auth = this.options.authMethod.toUpperCase().trim(); 538 | } else { 539 | // use first supported 540 | auth = (this._supportedAuth[0] || 'PLAIN').toUpperCase().trim(); 541 | } 542 | 543 | switch (auth) { 544 | case 'LOGIN': 545 | // LOGIN is a 3 step authentication process 546 | // C: AUTH LOGIN 547 | // C: BASE64(USER) 548 | // C: BASE64(PASS) 549 | this.logger.debug(DEBUG_TAG, 'Authentication via AUTH LOGIN'); 550 | this._currentAction = this._actionAUTH_LOGIN_USER; 551 | this._sendCommand('AUTH LOGIN'); 552 | return; 553 | case 'PLAIN': 554 | // AUTH PLAIN is a 1 step authentication process 555 | // C: AUTH PLAIN BASE64(\0 USER \0 PASS) 556 | this.logger.debug(DEBUG_TAG, 'Authentication via AUTH PLAIN'); 557 | this._currentAction = this._actionAUTHComplete; 558 | this._sendCommand( 559 | // convert to BASE64 560 | 'AUTH PLAIN ' + (0, _emailjsBase.encode)( 561 | // this.options.auth.user+'\u0000'+ 562 | '\0' + // skip authorization identity as it causes problems with some servers 563 | this.options.auth.user + '\0' + this.options.auth.pass)); 564 | return; 565 | case 'XOAUTH2': 566 | // See https://developers.google.com/gmail/xoauth2_protocol#smtp_protocol_exchange 567 | this.logger.debug(DEBUG_TAG, 'Authentication via AUTH XOAUTH2'); 568 | this._currentAction = this._actionAUTH_XOAUTH2; 569 | this._sendCommand('AUTH XOAUTH2 ' + this._buildXOAuth2Token(this.options.auth.user, this.options.auth.xoauth2)); 570 | return; 571 | } 572 | 573 | this._onError(new Error('Unknown authentication method ' + auth)); 574 | } 575 | 576 | // ACTIONS FOR RESPONSES FROM THE SMTP SERVER 577 | 578 | /** 579 | * Initial response from the server, must have a status 220 580 | * 581 | * @param {Object} command Parsed command from the server {statusCode, data, line} 582 | */ 583 | 584 | }, { 585 | key: '_actionGreeting', 586 | value: function _actionGreeting(command) { 587 | if (command.statusCode !== 220) { 588 | this._onError(new Error('Invalid greeting: ' + command.data)); 589 | return; 590 | } 591 | 592 | if (this.options.lmtp) { 593 | this.logger.debug(DEBUG_TAG, 'Sending LHLO ' + this.options.name); 594 | 595 | this._currentAction = this._actionLHLO; 596 | this._sendCommand('LHLO ' + this.options.name); 597 | } else { 598 | this.logger.debug(DEBUG_TAG, 'Sending EHLO ' + this.options.name); 599 | 600 | this._currentAction = this._actionEHLO; 601 | this._sendCommand('EHLO ' + this.options.name); 602 | } 603 | } 604 | 605 | /** 606 | * Response to LHLO 607 | * 608 | * @param {Object} command Parsed command from the server {statusCode, data, line} 609 | */ 610 | 611 | }, { 612 | key: '_actionLHLO', 613 | value: function _actionLHLO(command) { 614 | if (!command.success) { 615 | this.logger.error(DEBUG_TAG, 'LHLO not successful'); 616 | this._onError(new Error(command.data)); 617 | return; 618 | } 619 | 620 | // Process as EHLO response 621 | this._actionEHLO(command); 622 | } 623 | 624 | /** 625 | * Response to EHLO. If the response is an error, try HELO instead 626 | * 627 | * @param {Object} command Parsed command from the server {statusCode, data, line} 628 | */ 629 | 630 | }, { 631 | key: '_actionEHLO', 632 | value: function _actionEHLO(command) { 633 | var match; 634 | 635 | if (!command.success) { 636 | if (!this._secureMode && this.options.requireTLS) { 637 | var errMsg = 'STARTTLS not supported without EHLO'; 638 | this.logger.error(DEBUG_TAG, errMsg); 639 | this._onError(new Error(errMsg)); 640 | return; 641 | } 642 | 643 | // Try HELO instead 644 | this.logger.warn(DEBUG_TAG, 'EHLO not successful, trying HELO ' + this.options.name); 645 | this._currentAction = this._actionHELO; 646 | this._sendCommand('HELO ' + this.options.name); 647 | return; 648 | } 649 | 650 | // Detect if the server supports PLAIN auth 651 | if (command.line.match(/AUTH(?:\s+[^\n]*\s+|\s+)PLAIN/i)) { 652 | this.logger.debug(DEBUG_TAG, 'Server supports AUTH PLAIN'); 653 | this._supportedAuth.push('PLAIN'); 654 | } 655 | 656 | // Detect if the server supports LOGIN auth 657 | if (command.line.match(/AUTH(?:\s+[^\n]*\s+|\s+)LOGIN/i)) { 658 | this.logger.debug(DEBUG_TAG, 'Server supports AUTH LOGIN'); 659 | this._supportedAuth.push('LOGIN'); 660 | } 661 | 662 | // Detect if the server supports XOAUTH2 auth 663 | if (command.line.match(/AUTH(?:\s+[^\n]*\s+|\s+)XOAUTH2/i)) { 664 | this.logger.debug(DEBUG_TAG, 'Server supports AUTH XOAUTH2'); 665 | this._supportedAuth.push('XOAUTH2'); 666 | } 667 | 668 | // Detect maximum allowed message size 669 | if ((match = command.line.match(/SIZE (\d+)/i)) && Number(match[1])) { 670 | var maxAllowedSize = Number(match[1]); 671 | this.logger.debug(DEBUG_TAG, 'Maximum allowd message size: ' + maxAllowedSize); 672 | } 673 | 674 | // Detect if the server supports STARTTLS 675 | if (!this._secureMode) { 676 | if (command.line.match(/[ -]STARTTLS\s?$/mi) && !this.options.ignoreTLS || !!this.options.requireTLS) { 677 | this._currentAction = this._actionSTARTTLS; 678 | this.logger.debug(DEBUG_TAG, 'Sending STARTTLS'); 679 | this._sendCommand('STARTTLS'); 680 | return; 681 | } 682 | } 683 | 684 | this._authenticateUser(); 685 | } 686 | 687 | /** 688 | * Handles server response for STARTTLS command. If there's an error 689 | * try HELO instead, otherwise initiate TLS upgrade. If the upgrade 690 | * succeedes restart the EHLO 691 | * 692 | * @param {String} str Message from the server 693 | */ 694 | 695 | }, { 696 | key: '_actionSTARTTLS', 697 | value: function _actionSTARTTLS(command) { 698 | if (!command.success) { 699 | this.logger.error(DEBUG_TAG, 'STARTTLS not successful'); 700 | this._onError(new Error(command.data)); 701 | return; 702 | } 703 | 704 | this._secureMode = true; 705 | this.socket.upgradeToSecure(); 706 | 707 | // restart protocol flow 708 | this._currentAction = this._actionEHLO; 709 | this._sendCommand('EHLO ' + this.options.name); 710 | } 711 | 712 | /** 713 | * Response to HELO 714 | * 715 | * @param {Object} command Parsed command from the server {statusCode, data, line} 716 | */ 717 | 718 | }, { 719 | key: '_actionHELO', 720 | value: function _actionHELO(command) { 721 | if (!command.success) { 722 | this.logger.error(DEBUG_TAG, 'HELO not successful'); 723 | this._onError(new Error(command.data)); 724 | return; 725 | } 726 | this._authenticateUser(); 727 | } 728 | 729 | /** 730 | * Response to AUTH LOGIN, if successful expects base64 encoded username 731 | * 732 | * @param {Object} command Parsed command from the server {statusCode, data, line} 733 | */ 734 | 735 | }, { 736 | key: '_actionAUTH_LOGIN_USER', 737 | value: function _actionAUTH_LOGIN_USER(command) { 738 | if (command.statusCode !== 334 || command.data !== 'VXNlcm5hbWU6') { 739 | this.logger.error(DEBUG_TAG, 'AUTH LOGIN USER not successful: ' + command.data); 740 | this._onError(new Error('Invalid login sequence while waiting for "334 VXNlcm5hbWU6 ": ' + command.data)); 741 | return; 742 | } 743 | this.logger.debug(DEBUG_TAG, 'AUTH LOGIN USER successful'); 744 | this._currentAction = this._actionAUTH_LOGIN_PASS; 745 | this._sendCommand((0, _emailjsBase.encode)(this.options.auth.user)); 746 | } 747 | 748 | /** 749 | * Response to AUTH LOGIN username, if successful expects base64 encoded password 750 | * 751 | * @param {Object} command Parsed command from the server {statusCode, data, line} 752 | */ 753 | 754 | }, { 755 | key: '_actionAUTH_LOGIN_PASS', 756 | value: function _actionAUTH_LOGIN_PASS(command) { 757 | if (command.statusCode !== 334 || command.data !== 'UGFzc3dvcmQ6') { 758 | this.logger.error(DEBUG_TAG, 'AUTH LOGIN PASS not successful: ' + command.data); 759 | this._onError(new Error('Invalid login sequence while waiting for "334 UGFzc3dvcmQ6 ": ' + command.data)); 760 | return; 761 | } 762 | this.logger.debug(DEBUG_TAG, 'AUTH LOGIN PASS successful'); 763 | this._currentAction = this._actionAUTHComplete; 764 | this._sendCommand((0, _emailjsBase.encode)(this.options.auth.pass)); 765 | } 766 | 767 | /** 768 | * Response to AUTH XOAUTH2 token, if error occurs send empty response 769 | * 770 | * @param {Object} command Parsed command from the server {statusCode, data, line} 771 | */ 772 | 773 | }, { 774 | key: '_actionAUTH_XOAUTH2', 775 | value: function _actionAUTH_XOAUTH2(command) { 776 | if (!command.success) { 777 | this.logger.warn(DEBUG_TAG, 'Error during AUTH XOAUTH2, sending empty response'); 778 | this._sendCommand(''); 779 | this._currentAction = this._actionAUTHComplete; 780 | } else { 781 | this._actionAUTHComplete(command); 782 | } 783 | } 784 | 785 | /** 786 | * Checks if authentication succeeded or not. If successfully authenticated 787 | * emit `idle` to indicate that an e-mail can be sent using this connection 788 | * 789 | * @param {Object} command Parsed command from the server {statusCode, data, line} 790 | */ 791 | 792 | }, { 793 | key: '_actionAUTHComplete', 794 | value: function _actionAUTHComplete(command) { 795 | if (!command.success) { 796 | this.logger.debug(DEBUG_TAG, 'Authentication failed: ' + command.data); 797 | this._onError(new Error(command.data)); 798 | return; 799 | } 800 | 801 | this.logger.debug(DEBUG_TAG, 'Authentication successful.'); 802 | 803 | this._authenticatedAs = this.options.auth.user; 804 | 805 | this._currentAction = this._actionIdle; 806 | this.onidle(); // ready to take orders 807 | } 808 | 809 | /** 810 | * Used when the connection is idle and the server emits timeout 811 | * 812 | * @param {Object} command Parsed command from the server {statusCode, data, line} 813 | */ 814 | 815 | }, { 816 | key: '_actionIdle', 817 | value: function _actionIdle(command) { 818 | if (command.statusCode > 300) { 819 | this._onError(new Error(command.line)); 820 | return; 821 | } 822 | 823 | this._onError(new Error(command.data)); 824 | } 825 | 826 | /** 827 | * Response to MAIL FROM command. Proceed to defining RCPT TO list if successful 828 | * 829 | * @param {Object} command Parsed command from the server {statusCode, data, line} 830 | */ 831 | 832 | }, { 833 | key: '_actionMAIL', 834 | value: function _actionMAIL(command) { 835 | if (!command.success) { 836 | this.logger.debug(DEBUG_TAG, 'MAIL FROM unsuccessful: ' + command.data); 837 | this._onError(new Error(command.data)); 838 | return; 839 | } 840 | 841 | if (!this._envelope.rcptQueue.length) { 842 | this._onError(new Error('Can\'t send mail - no recipients defined')); 843 | } else { 844 | this.logger.debug(DEBUG_TAG, 'MAIL FROM successful, proceeding with ' + this._envelope.rcptQueue.length + ' recipients'); 845 | this.logger.debug(DEBUG_TAG, 'Adding recipient...'); 846 | this._envelope.curRecipient = this._envelope.rcptQueue.shift(); 847 | this._currentAction = this._actionRCPT; 848 | this._sendCommand('RCPT TO:<' + this._envelope.curRecipient + '>'); 849 | } 850 | } 851 | 852 | /** 853 | * Response to a RCPT TO command. If the command is unsuccessful, try the next one, 854 | * as this might be related only to the current recipient, not a global error, so 855 | * the following recipients might still be valid 856 | * 857 | * @param {Object} command Parsed command from the server {statusCode, data, line} 858 | */ 859 | 860 | }, { 861 | key: '_actionRCPT', 862 | value: function _actionRCPT(command) { 863 | if (!command.success) { 864 | this.logger.warn(DEBUG_TAG, 'RCPT TO failed for: ' + this._envelope.curRecipient); 865 | // this is a soft error 866 | this._envelope.rcptFailed.push(this._envelope.curRecipient); 867 | } else { 868 | this._envelope.responseQueue.push(this._envelope.curRecipient); 869 | } 870 | 871 | if (!this._envelope.rcptQueue.length) { 872 | if (this._envelope.rcptFailed.length < this._envelope.to.length) { 873 | this._currentAction = this._actionDATA; 874 | this.logger.debug(DEBUG_TAG, 'RCPT TO done, proceeding with payload'); 875 | this._sendCommand('DATA'); 876 | } else { 877 | this._onError(new Error('Can\'t send mail - all recipients were rejected')); 878 | this._currentAction = this._actionIdle; 879 | } 880 | } else { 881 | this.logger.debug(DEBUG_TAG, 'Adding recipient...'); 882 | this._envelope.curRecipient = this._envelope.rcptQueue.shift(); 883 | this._currentAction = this._actionRCPT; 884 | this._sendCommand('RCPT TO:<' + this._envelope.curRecipient + '>'); 885 | } 886 | } 887 | 888 | /** 889 | * Response to the RSET command. If successful, clear the current authentication 890 | * information and reauthenticate. 891 | * 892 | * @param {Object} command Parsed command from the server {statusCode, data, line} 893 | */ 894 | 895 | }, { 896 | key: '_actionRSET', 897 | value: function _actionRSET(command) { 898 | if (!command.success) { 899 | this.logger.error(DEBUG_TAG, 'RSET unsuccessful ' + command.data); 900 | this._onError(new Error(command.data)); 901 | return; 902 | } 903 | 904 | this._authenticatedAs = null; 905 | this._authenticateUser(); 906 | } 907 | 908 | /** 909 | * Response to the DATA command. Server is now waiting for a message, so emit `onready` 910 | * 911 | * @param {Object} command Parsed command from the server {statusCode, data, line} 912 | */ 913 | 914 | }, { 915 | key: '_actionDATA', 916 | value: function _actionDATA(command) { 917 | // response should be 354 but according to this issue https://github.com/eleith/emailjs/issues/24 918 | // some servers might use 250 instead 919 | if ([250, 354].indexOf(command.statusCode) < 0) { 920 | this.logger.error(DEBUG_TAG, 'DATA unsuccessful ' + command.data); 921 | this._onError(new Error(command.data)); 922 | return; 923 | } 924 | 925 | this._dataMode = true; 926 | this._currentAction = this._actionIdle; 927 | this.onready(this._envelope.rcptFailed); 928 | } 929 | 930 | /** 931 | * Response from the server, once the message stream has ended with . 932 | * Emits `ondone`. 933 | * 934 | * @param {Object} command Parsed command from the server {statusCode, data, line} 935 | */ 936 | 937 | }, { 938 | key: '_actionStream', 939 | value: function _actionStream(command) { 940 | var rcpt; 941 | 942 | if (this.options.lmtp) { 943 | // LMTP returns a response code for *every* successfully set recipient 944 | // For every recipient the message might succeed or fail individually 945 | 946 | rcpt = this._envelope.responseQueue.shift(); 947 | if (!command.success) { 948 | this.logger.error(DEBUG_TAG, 'Local delivery to ' + rcpt + ' failed.'); 949 | this._envelope.rcptFailed.push(rcpt); 950 | } else { 951 | this.logger.error(DEBUG_TAG, 'Local delivery to ' + rcpt + ' succeeded.'); 952 | } 953 | 954 | if (this._envelope.responseQueue.length) { 955 | this._currentAction = this._actionStream; 956 | return; 957 | } 958 | 959 | this._currentAction = this._actionIdle; 960 | this.ondone(true); 961 | } else { 962 | // For SMTP the message either fails or succeeds, there is no information 963 | // about individual recipients 964 | 965 | if (!command.success) { 966 | this.logger.error(DEBUG_TAG, 'Message sending failed.'); 967 | } else { 968 | this.logger.debug(DEBUG_TAG, 'Message sent successfully.'); 969 | } 970 | 971 | this._currentAction = this._actionIdle; 972 | this.ondone(!!command.success); 973 | } 974 | 975 | // If the client wanted to do something else (eg. to quit), do not force idle 976 | if (this._currentAction === this._actionIdle) { 977 | // Waiting for new connections 978 | this.logger.debug(DEBUG_TAG, 'Idling while waiting for new connections...'); 979 | this.onidle(); 980 | } 981 | } 982 | 983 | /** 984 | * Builds a login token for XOAUTH2 authentication command 985 | * 986 | * @param {String} user E-mail address of the user 987 | * @param {String} token Valid access token for the user 988 | * @return {String} Base64 formatted login token 989 | */ 990 | 991 | }, { 992 | key: '_buildXOAuth2Token', 993 | value: function _buildXOAuth2Token(user, token) { 994 | var authData = ['user=' + (user || ''), 'auth=Bearer ' + token, '', '']; 995 | // base64("user={User}\x00auth=Bearer {Token}\x00\x00") 996 | return (0, _emailjsBase.encode)(authData.join('\x01')); 997 | } 998 | }, { 999 | key: 'createLogger', 1000 | value: function createLogger() { 1001 | var _this = this; 1002 | 1003 | var creator = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _logger2.default; 1004 | 1005 | var logger = creator((this.options.auth || {}).user || '', this.host); 1006 | this.logLevel = this.LOG_LEVEL_ALL; 1007 | this.logger = { 1008 | debug: function debug() { 1009 | for (var _len = arguments.length, msgs = Array(_len), _key = 0; _key < _len; _key++) { 1010 | msgs[_key] = arguments[_key]; 1011 | } 1012 | 1013 | if (_common.LOG_LEVEL_DEBUG >= _this.logLevel) { 1014 | logger.debug(msgs); 1015 | } 1016 | }, 1017 | info: function info() { 1018 | for (var _len2 = arguments.length, msgs = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 1019 | msgs[_key2] = arguments[_key2]; 1020 | } 1021 | 1022 | if (_common.LOG_LEVEL_INFO >= _this.logLevel) { 1023 | logger.info(msgs); 1024 | } 1025 | }, 1026 | warn: function warn() { 1027 | for (var _len3 = arguments.length, msgs = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { 1028 | msgs[_key3] = arguments[_key3]; 1029 | } 1030 | 1031 | if (_common.LOG_LEVEL_WARN >= _this.logLevel) { 1032 | logger.warn(msgs); 1033 | } 1034 | }, 1035 | error: function error() { 1036 | for (var _len4 = arguments.length, msgs = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { 1037 | msgs[_key4] = arguments[_key4]; 1038 | } 1039 | 1040 | if (_common.LOG_LEVEL_ERROR >= _this.logLevel) { 1041 | logger.error(msgs); 1042 | } 1043 | } 1044 | }; 1045 | } 1046 | }]); 1047 | 1048 | return SmtpClient; 1049 | }(); 1050 | 1051 | exports.default = SmtpClient; 1052 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9jbGllbnQuanMiXSwibmFtZXMiOlsiREVCVUdfVEFHIiwiVElNRU9VVF9TT0NLRVRfTE9XRVJfQk9VTkQiLCJUSU1FT1VUX1NPQ0tFVF9NVUxUSVBMSUVSIiwiU210cENsaWVudCIsImhvc3QiLCJwb3J0Iiwib3B0aW9ucyIsInRpbWVvdXRTb2NrZXRMb3dlckJvdW5kIiwidGltZW91dFNvY2tldE11bHRpcGxpZXIiLCJ1c2VTZWN1cmVUcmFuc3BvcnQiLCJhdXRoIiwibmFtZSIsInNvY2tldCIsImRlc3Ryb3llZCIsIndhaXREcmFpbiIsIl9wYXJzZXIiLCJTbXRwQ2xpZW50UmVzcG9uc2VQYXJzZXIiLCJfYXV0aGVudGljYXRlZEFzIiwiX3N1cHBvcnRlZEF1dGgiLCJfZGF0YU1vZGUiLCJfbGFzdERhdGFCeXRlcyIsIl9lbnZlbG9wZSIsIl9jdXJyZW50QWN0aW9uIiwiX3NlY3VyZU1vZGUiLCJfc29ja2V0VGltZW91dFRpbWVyIiwiX3NvY2tldFRpbWVvdXRTdGFydCIsIl9zb2NrZXRUaW1lb3V0UGVyaW9kIiwiY3JlYXRlTG9nZ2VyIiwib25lcnJvciIsImUiLCJvbmRyYWluIiwib25jbG9zZSIsIm9uaWRsZSIsIm9ucmVhZHkiLCJmYWlsZWRSZWNpcGllbnRzIiwib25kb25lIiwic3VjY2VzcyIsIlNvY2tldENvbnRydWN0b3IiLCJUQ1BTb2NrZXQiLCJvcGVuIiwiYmluYXJ5VHlwZSIsImNhIiwidGxzV29ya2VyUGF0aCIsIndzIiwib25jZXJ0IiwiRSIsIl9vbkVycm9yIiwiYmluZCIsIm9ub3BlbiIsIl9vbk9wZW4iLCJyZWFkeVN0YXRlIiwic3VzcGVuZCIsInJlc3VtZSIsImxvZ2dlciIsImRlYnVnIiwiX3NlbmRDb21tYW5kIiwiY2xvc2UiLCJfYWN0aW9uUlNFVCIsIl9kZXN0cm95IiwiZW52ZWxvcGUiLCJmcm9tIiwiY29uY2F0IiwidG8iLCJyY3B0UXVldWUiLCJyY3B0RmFpbGVkIiwicmVzcG9uc2VRdWV1ZSIsIl9hY3Rpb25NQUlMIiwiY2h1bmsiLCJfc2VuZFN0cmluZyIsImxlbmd0aCIsInNlbmQiLCJfYWN0aW9uU3RyZWFtIiwiX3NlbmQiLCJVaW50OEFycmF5IiwiYnVmZmVyIiwic3Vic3RyIiwiZXZlbnQiLCJkYXRhIiwicHJveHlIb3N0bmFtZSIsIm9uZGF0YSIsIl9vbkRhdGEiLCJfb25DbG9zZSIsIl9vbkRyYWluIiwiX29uQ29tbWFuZCIsIl9hY3Rpb25HcmVldGluZyIsImV2dCIsImNsZWFyVGltZW91dCIsInN0cmluZ1BheWxvYWQiLCJUZXh0RGVjb2RlciIsImRlY29kZSIsIkVycm9yIiwibWVzc2FnZSIsImVycm9yIiwiY29tbWFuZCIsImRpc2FibGVFc2NhcGluZyIsInJlcGxhY2UiLCJjaGFyQXQiLCJUZXh0RW5jb2RlciIsImVuY29kZSIsInN0ciIsIl9zZXRUaW1lb3V0IiwiYnl0ZUxlbmd0aCIsInByb2xvbmdQZXJpb2QiLCJNYXRoIiwiZmxvb3IiLCJ0aW1lb3V0Iiwibm93IiwiRGF0ZSIsInNldFRpbWVvdXQiLCJfb25UaW1lb3V0IiwiX2FjdGlvbklkbGUiLCJhdXRoTWV0aG9kIiwieG9hdXRoMiIsInRvVXBwZXJDYXNlIiwidHJpbSIsIl9hY3Rpb25BVVRIX0xPR0lOX1VTRVIiLCJfYWN0aW9uQVVUSENvbXBsZXRlIiwidXNlciIsInBhc3MiLCJfYWN0aW9uQVVUSF9YT0FVVEgyIiwiX2J1aWxkWE9BdXRoMlRva2VuIiwic3RhdHVzQ29kZSIsImxtdHAiLCJfYWN0aW9uTEhMTyIsIl9hY3Rpb25FSExPIiwibWF0Y2giLCJyZXF1aXJlVExTIiwiZXJyTXNnIiwid2FybiIsIl9hY3Rpb25IRUxPIiwibGluZSIsInB1c2giLCJOdW1iZXIiLCJtYXhBbGxvd2VkU2l6ZSIsImlnbm9yZVRMUyIsIl9hY3Rpb25TVEFSVFRMUyIsIl9hdXRoZW50aWNhdGVVc2VyIiwidXBncmFkZVRvU2VjdXJlIiwiX2FjdGlvbkFVVEhfTE9HSU5fUEFTUyIsImN1clJlY2lwaWVudCIsInNoaWZ0IiwiX2FjdGlvblJDUFQiLCJfYWN0aW9uREFUQSIsImluZGV4T2YiLCJyY3B0IiwidG9rZW4iLCJhdXRoRGF0YSIsImpvaW4iLCJjcmVhdG9yIiwiY3JlYXRlRGVmYXVsdExvZ2dlciIsImxvZ0xldmVsIiwiTE9HX0xFVkVMX0FMTCIsIm1zZ3MiLCJMT0dfTEVWRUxfREVCVUciLCJpbmZvIiwiTE9HX0xFVkVMX0lORk8iLCJMT0dfTEVWRUxfV0FSTiIsIkxPR19MRVZFTF9FUlJPUiJdLCJtYXBwaW5ncyI6Ijs7Ozs7O3FqQkFBQTs7QUFFQTs7QUFDQTs7OztBQUNBOztBQUNBOzs7O0FBQ0E7Ozs7QUFDQTs7Ozs7O0FBT0EsSUFBSUEsWUFBWSxhQUFoQjs7QUFFQTs7O0FBR0EsSUFBTUMsNkJBQTZCLEtBQW5DOztBQUVBOzs7Ozs7O0FBT0EsSUFBTUMsNEJBQTRCLEdBQWxDOztJQUVNQyxVO0FBQ0o7Ozs7Ozs7Ozs7Ozs7Ozs7OztBQWtCQSxzQkFBYUMsSUFBYixFQUFtQkMsSUFBbkIsRUFBdUM7QUFBQSxRQUFkQyxPQUFjLHVFQUFKLEVBQUk7O0FBQUE7O0FBQ3JDLFNBQUtBLE9BQUwsR0FBZUEsT0FBZjs7QUFFQSxTQUFLQyx1QkFBTCxHQUErQk4sMEJBQS9CO0FBQ0EsU0FBS08sdUJBQUwsR0FBK0JOLHlCQUEvQjs7QUFFQSxTQUFLRyxJQUFMLEdBQVlBLFNBQVMsS0FBS0MsT0FBTCxDQUFhRyxrQkFBYixHQUFrQyxHQUFsQyxHQUF3QyxFQUFqRCxDQUFaO0FBQ0EsU0FBS0wsSUFBTCxHQUFZQSxRQUFRLFdBQXBCOztBQUVBOzs7OztBQUtBLFNBQUtFLE9BQUwsQ0FBYUcsa0JBQWIsR0FBa0Msd0JBQXdCLEtBQUtILE9BQTdCLEdBQXVDLENBQUMsQ0FBQyxLQUFLQSxPQUFMLENBQWFHLGtCQUF0RCxHQUEyRSxLQUFLSixJQUFMLEtBQWMsR0FBM0g7O0FBRUEsU0FBS0MsT0FBTCxDQUFhSSxJQUFiLEdBQW9CLEtBQUtKLE9BQUwsQ0FBYUksSUFBYixJQUFxQixLQUF6QyxDQWhCcUMsQ0FnQlU7QUFDL0MsU0FBS0osT0FB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QStCLHVFQUFyQkMsZ0JBQXFCOztBQUMzQyxVQUFNaEcsU0FBUytGLFFBQVEsQ0FBQyxLQUFLOUksT0FBTCxDQUFhSSxJQUFiLElBQXFCLEVBQXRCLEVBQTBCMkcsSUFBMUIsSUFBa0MsRUFBMUMsRUFBOEMsS0FBS2pILElBQW5ELENBQWY7QUFDQSxXQUFLa0osUUFBTCxHQUFnQixLQUFLQyxhQUFyQjtBQUNBLFdBQUtsRyxNQUFMLEdBQWM7QUFDWkMsZUFBTyxpQkFBYTtBQUFBLDRDQUFUa0csSUFBUztBQUFUQSxnQkFBUztBQUFBOztBQUFFLGNBQUlDLDJCQUFtQixNQUFLSCxRQUE1QixFQUFzQztBQUFFakcsbUJBQU9DLEtBQVAsQ0FBYWtHLElBQWI7QUFBb0I7QUFBRSxTQUR4RTtBQUVaRSxjQUFNLGdCQUFhO0FBQUEsNkNBQVRGLElBQVM7QUFBVEEsZ0JBQVM7QUFBQTs7QUFBRSxjQUFJRywwQkFBa0IsTUFBS0wsUUFBM0IsRUFBcUM7QUFBRWpHLG1CQUFPcUcsSUFBUCxDQUFZRixJQUFaO0FBQW1CO0FBQUUsU0FGckU7QUFHWnhCLGNBQU0sZ0JBQWE7QUFBQSw2Q0FBVHdCLElBQVM7QUFBVEEsZ0JBQVM7QUFBQTs7QUFBRSxjQUFJSSwwQkFBa0IsTUFBS04sUUFBM0IsRUFBcUM7QUFBRWpHLG1CQUFPMkUsSUFBUCxDQUFZd0IsSUFBWjtBQUFtQjtBQUFFLFNBSHJFO0FBSVo1RCxlQUFPLGlCQUFhO0FBQUEsNkNBQVQ0RCxJQUFTO0FBQVRBLGdCQUFTO0FBQUE7O0FBQUUsY0FBSUssMkJBQW1CLE1BQUtQLFFBQTVCLEVBQXNDO0FBQUVqRyxtQkFBT3VDLEtBQVAsQ0FBYTRELElBQWI7QUFBb0I7QUFBRTtBQUp4RSxPQUFkO0FBTUQ7Ozs7OztrQkFHWXJKLFUiLCJmaWxlIjoiY2xpZW50LmpzIiwic291cmNlc0NvbnRlbnQiOlsiLyogZXNsaW50LWRpc2FibGUgY2FtZWxjYXNlICovXG5cbmltcG9ydCB7IGVuY29kZSB9IGZyb20gJ2VtYWlsanMtYmFzZTY0J1xuaW1wb3J0IFRDUFNvY2tldCBmcm9tICdlbWFpbGpzLXRjcC1zb2NrZXQnXG5pbXBvcnQgeyBUZXh0RGVjb2RlciwgVGV4dEVuY29kZXIgfSBmcm9tICd0ZXh0LWVuY29kaW5nJ1xuaW1wb3J0IFNtdHBDbGllbnRSZXNwb25zZVBhcnNlciBmcm9tICcuL3BhcnNlcidcbmltcG9ydCBjcmVhdGVEZWZhdWx0TG9nZ2VyIGZyb20gJy4vbG9nZ2VyJ1xuaW1wb3J0IHtcbiAgTE9HX0xFVkVMX0VSUk9SLFxuICBMT0dfTEVWRUxfV0FSTixcbiAgTE9HX0xFVkVMX0lORk8sXG4gIExPR19MRVZFTF9ERUJVR1xufSBmcm9tICcuL2NvbW1vbidcblxudmFyIERFQlVHX1RBRyA9ICdTTVRQIENsaWVudCdcblxuLyoqXG4gKiBMb3dlciBCb3VuZCBmb3Igc29ja2V0IHRpbWVvdXQgdG8gd2FpdCBzaW5jZSB0aGUgbGFzdCBkYXRhIHdhcyB3cml0dGVuIHRvIGEgc29ja2V0XG4gKi9cbmNvbnN0IFRJTUVPVVRfU09DS0VUX0xPV0VSX0JPVU5EID0gMTAwMDBcblxuLyoqXG4gKiBNdWx0aXBsaWVyIGZvciBzb2NrZXQgdGltZW91dDpcbiAqXG4gKiBXZSBhc3N1bWUgYXQgbGVhc3QgYSBHUFJTIGNvbm5lY3Rpb24gd2l0aCAxMTUga2IvcyA9IDE0LDM3NSBrQi9zIHRvcHMsIHNvIDEwIEtCL3MgdG8gYmUgb25cbiAqIHRoZSBzYWZlIHNpZGUuIFdlIGNhbiB0aW1lb3V0IGFmdGVyIGEgbG93ZXIgYm91bmQgb2YgMTBzICsgKG4gS0IgLyAxMCBLQi9zKS4gQSAxIE1CIG1lc3NhZ2VcbiAqIHVwbG9hZCB3b3VsZCBiZSAxMTAgc2Vjb25kcyB0byB3YWl0IGZvciB0aGUgdGltZW91dC4gMTAgS0IvcyA9PT0gMC4xIHMvQlxuICovXG5jb25zdCBUSU1FT1VUX1NPQ0tFVF9NVUxUSVBMSUVSID0gMC4xXG5cbmNsYXNzIFNtdHBDbGllbnQge1xuICAvKipcbiAgICogQ3JlYXRlcyBhIGNvbm5lY3Rpb24gb2JqZWN0IHRvIGEgU01UUCBzZXJ2ZXIgYW5kIGFsbG93cyB0byBzZW5kIG1haWwgdGhyb3VnaCBpdC5cbiAgICogQ2FsbCBgY29ubmVjdGAgbWV0aG9kIHRvIGluaXRpdGF0ZSB0aGUgYWN0dWFsIGNvbm5lY3Rpb24sIHRoZSBjb25zdHJ1Y3RvciBvbmx5XG4gICAqIGRlZmluZXMgdGhlIHByb3BlcnRpZXMgYnV0IGRvZXMgbm90IGFjdHVhbGx5IGNvbm5lY3QuXG4gICAqXG4gICAqIE5CISBUaGUgcGFyYW1ldGVyIG9yZGVyIChob3N0LCBwb3J0KSBkaWZmZXJzIGZyb20gbm9kZS5qcyBcIndheVwiIChwb3J0LCBob3N0KVxuICAgKlxuICAgKiBAY29uc3RydWN0b3JcbiAgICpcbiAgICogQHBhcmFtIHtTdHJpbmd9IFtob3N0PVwibG9jYWxob3N0XCJdIEhvc3RuYW1lIHRvIGNvbmVuY3QgdG9cbiAgICogQHBhcmFtIHtOdW1iZXJ9IFtwb3J0PTI1XSBQb3J0IG51bWJlciB0byBjb25uZWN0IHRvXG4gICAqIEBwYXJhbSB7T2JqZWN0fSBbb3B0aW9uc10gT3B0aW9uYWwgb3B0aW9ucyBvYmplY3RcbiAgICogQHBhcmFtIHtCb29sZWFufSBbb3B0aW9ucy51c2VTZWN1cmVUcmFuc3BvcnRdIFNldCB0byB0cnVlLCB0byB1c2UgZW5jcnlwdGVkIGNvbm5lY3Rpb25cbiAgICogQHBhcmFtIHtTdHJpbmd9IFtvcHRpb25zLm5hbWVdIENsaWVudCBob3N0bmFtZSBmb3IgaW50cm9kdWNpbmcgaXRzZWxmIHRvIHRoZSBzZXJ2ZXJcbiAgICogQHBhcmFtIHtPYmplY3R9IFtvcHRpb25zLmF1dGhdIEF1dGhlbnRpY2F0aW9uIG9wdGlvbnMuIERlcGVuZHMgb24gdGhlIHByZWZlcnJlZCBhdXRoZW50aWNhdGlvbiBtZXRob2QuIFVzdWFsbHkge3VzZXIsIHBhc3N9XG4gICAqIEBwYXJhbSB7U3RyaW5nfSBbb3B0aW9ucy5hdXRoTWV0aG9kXSBGb3JjZSBzcGVjaWZpYyBhdXRoZW50aWNhdGlvbiBtZXRob2RcbiAgICogQHBhcmFtIHtCb29sZWFufSBbb3B0aW9ucy5kaXNhYmxlRXNjYXBpbmddIElmIHNldCB0byB0cnVlLCBkbyBub3QgZXNjYXBlIGRvdHMgb24gdGhlIGJlZ2lubmluZyBvZiB0aGUgbGluZXNcbiAgICovXG4gIGNvbnN0cnVjdG9yIChob3N0LCBwb3J0LCBvcHRpb25zID0ge30pIHtcbiAgICB0aGlzLm9wdGlvbnMgPSBvcHRpb25zXG5cbiAgICB0aGlzLnRpbWVvdXRTb2NrZXRMb3dlckJvdW5kID0gVElNRU9VVF9TT0NLRVRfTE9XRVJfQk9VTkRcbiAgICB0aGlzLnRpbWVvdXRTb2NrZXRNdWx0aXBsaWVyID0gVElNRU9VVF9TT0NLRVRfTVVMVElQTElFUlxuXG4gICAgdGhpcy5wb3J0ID0gcG9ydCB8fCAodGhpcy5vcHRpb25zLnVzZVNlY3VyZVRyYW5zcG9ydCA/IDQ2NSA6IDI1KVxuICAgIHRoaXMuaG9zdCA9IGhvc3QgfHwgJ2xvY2FsaG9zdCdcblxuICAgIC8qKlxuICAgICAqIElmIHNldCB0byB0cnVlLCBzdGFydCBhbiBlbmNyeXB0ZWQgY29ubmVjdGlvbiBpbnN0ZWFkIG9mIHRoZSBwbGFpbnRleHQgb25lXG4gICAgICogKHJlY29tbWVuZGVkIGlmIGFwcGxpY2FibGUpLiBJZiB1c2VTZWN1cmVUcmFuc3BvcnQgaXMgbm90IHNldCBidXQgdGhlIHBvcnQgdXNlZCBpcyA0NjUsXG4gICAgICogdGhlbiBlY3J5cHRpb24gaXMgdXNlZCBieSBkZWZhdWx0LlxuICAgICAqL1xuICAgIHRoaXMub3B0aW9ucy51c2VTZWN1cmVUcmFuc3BvcnQgPSAndXNlU2VjdXJlVHJhbnNwb3J0JyBpbiB0aGlzLm9wdGlvbnMgPyAhIXRoaXMub3B0aW9ucy51c2VTZWN1cmVUcmFuc3BvcnQgOiB0aGlzLnBvcnQgPT09IDQ2NVxuXG4gICAgdGhpcy5vcHRpb25zLmF1dGggPSB0aGlzLm9wdGlvbnMuYXV0aCB8fCBmYWxzZSAvLyBBdXRoZW50aWNhdGlvbiBvYmplY3QuIElmIG5vdCBzZXQsIGF1dGhlbnRpY2F0aW9uIHN0ZXAgd2lsbCBiZSBza2lwcGVkLlxuICAgIHRoaXMub3B0aW9ucy5uYW1lID0gdGhpcy5vcHRpb25zLm5hbWUgfHwgJ2xvY2FsaG9zdCcgLy8gSG9zdG5hbWUgb2YgdGhlIGNsaWVudCwgdGhpcyB3aWxsIGJlIHVzZWQgZm9yIGludHJvZHVjaW5nIHRvIHRoZSBzZXJ2ZXJcbiAgICB0aGlzLnNvY2tldCA9IGZhbHNlIC8vIERvd25zdHJlYW0gVENQIHNvY2tldCB0byB0aGUgU01UUCBzZXJ2ZXIsIGNyZWF0ZWQgd2l0aCBtb3pUQ1BTb2NrZXRcbiAgICB0aGlzLmRlc3Ryb3llZCA9IGZhbHNlIC8vIEluZGljYXRlcyBpZiB0aGUgY29ubmVjdGlvbiBoYXMgYmVlbiBjbG9zZWQgYW5kIGNhbid0IGJlIHVzZWQgYW55bW9yZVxuICAgIHRoaXMud2FpdERyYWluID0gZmFsc2UgLy8gS2VlcHMgdHJhY2sgaWYgdGhlIGRvd25zdHJlYW0gc29ja2V0IGlzIGN1cnJlbnRseSBmdWxsIGFuZCBhIGRyYWluIGV2ZW50IHNob3VsZCBiZSB3YWl0ZWQgZm9yIG9yIG5vdFxuXG4gICAgLy8gUHJpdmF0ZSBwcm9wZXJ0aWVzXG5cbiAgICB0aGlzLl9wYXJzZXIgPSBuZXcgU210cENsaWVudFJlc3BvbnNlUGFyc2VyKCkgLy8gU01UUCByZXNwb25zZSBwYXJzZXIgb2JqZWN0LiBBbGwgZGF0YSBjb21pbmcgZnJvbSB0aGUgZG93bnN0cmVhbSBzZXJ2ZXIgaXMgZmVlZGVkIHRvIHRoaXMgcGFyc2VyXG4gICAgdGhpcy5fYXV0aGVudGljYXRlZEFzID0gbnVsbCAvLyBJZiBhdXRoZW50aWNhdGVkIHN1Y2Nlc3NmdWxseSwgc3RvcmVzIHRoZSB1c2VybmFtZVxuICAgIHRoaXMuX3N1cHBvcnRlZEF1dGggPSBbXSAvLyBBIGxpc3Qgb2YgYXV0aGVudGljYXRpb24gbWVjaGFuaXNtcyBkZXRlY3RlZCBmcm9tIHRoZSBFSExPIHJlc3BvbnNlIGFuZCB3aGljaCBhcmUgY29tcGF0aWJsZSB3aXRoIHRoaXMgbGlicmFyeVxuICAgIHRoaXMuX2RhdGFNb2RlID0gZmFsc2UgLy8gSWYgdHJ1ZSwgYWNjZXB0cyBkYXRhIGZyb20gdGhlIHVwc3RyZWFtIHRvIGJlIHBhc3NlZCBkaXJlY3RseSB0byB0aGUgZG93bnN0cmVhbSBzb2NrZXQuIFVzZWQgYWZ0ZXIgdGhlIERBVEEgY29tbWFuZFxuICAgIHRoaXMuX2xhc3REYXRhQnl0ZXMgPSAnJyAvLyBLZWVwIHRyYWNrIG9mIHRoZSBsYXN0IGJ5dGVzIHRvIHNlZSBob3cgdGhlIHRlcm1pbmF0aW5nIGRvdCBzaG91bGQgYmUgcGxhY2VkXG4gICAgdGhpcy5fZW52ZWxvcGUgPSBudWxsIC8vIEVudmVsb3BlIG9iamVjdCBmb3IgdHJhY2tpbmcgd2hvIGlzIHNlbmRpbmcgbWFpbCB0byB3aG9tXG4gICAgdGhpcy5fY3VycmVudEFjdGlvbiA9IG51bGwgLy8gU3RvcmVzIHRoZSBmdW5jdGlvbiB0aGF0IHNob3VsZCBiZSBydW4gYWZ0ZXIgYSByZXNwb25zZSBoYXMgYmVlbiByZWNlaXZlZCBmcm9tIHRoZSBzZXJ2ZXJcbiAgICB0aGlzLl9zZWN1cmVNb2RlID0gISF0aGlzLm9wdGlvbnMudXNlU2VjdXJlVHJhbnNwb3J0IC8vIEluZGljYXRlcyBpZiB0aGUgY29ubmVjdGlvbiBpcyBzZWN1cmVkIG9yIHBsYWludGV4dFxuICAgIHRoaXMuX3NvY2tldFRpbWVvdXRUaW1lciA9IGZhbHNlIC8vIFRpbWVyIHdhaXRpbmcgdG8gZGVjbGFyZSB0aGUgc29ja2V0IGRlYWQgc3RhcnRpbmcgZnJvbSB0aGUgbGFzdCB3cml0ZVxuICAgIHRoaXMuX3NvY2tldFRpbWVvdXRTdGFydCA9IGZhbHNlIC8vIFN0YXJ0IHRpbWUgb2Ygc2VuZGluZyB0aGUgZmlyc3QgcGFja2V0IGluIGRhdGEgbW9kZVxuICAgIHRoaXMuX3NvY2tldFRpbWVvdXRQZXJpb2QgPSBmYWxzZSAvLyBUaW1lb3V0IGZvciBzZW5kaW5nIGluIGRhdGEgbW9kZSwgZ2V0cyBleHRlbmRlZCB3aXRoIGV2ZXJ5IHNlbmQoKVxuXG4gICAgLy8gQWN0aXZhdGUgbG9nZ2luZ1xuICAgIHRoaXMuY3JlYXRlTG9nZ2VyKClcblxuICAgIC8vIEV2ZW50IHBsYWNlaG9sZGVyc1xuICAgIHRoaXMub25lcnJvciA9IChlKSA9PiB7IH0gLy8gV2lsbCBiZSBydW4gd2hlbiBhbiBlcnJvciBvY2N1cnMuIFRoZSBgb25jbG9zZWAgZXZlbnQgd2lsbCBmaXJlIHN1YnNlcXVlbnRseS5cbiAgICB0aGlzLm9uZHJhaW4gPSAoKSA9PiB7IH0gLy8gTW9yZSBkYXRhIGNhbiBiZSBidWZmZXJlZCBpbiB0aGUgc29ja2V0LlxuICAgIHRoaXMub25jbG9zZSA9ICgpID0+IHsgfSAvLyBUaGUgY29ubmVjdGlvbiB0byB0aGUgc2VydmVyIGhhcyBiZWVuIGNsb3NlZFxuICAgIHRoaXMub25pZGxlID0gKCkgPT4geyB9IC8vIFRoZSBjb25uZWN0aW9uIGlzIGVzdGFibGlzaGVkIGFuZCBpZGxlLCB5b3UgY2FuIHNlbmQgbWFpbCBub3dcbiAgICB0aGlzLm9ucmVhZHkgPSAoZmFpbGVkUmVjaXBpZW50cykgPT4geyB9IC8vIFdhaXRpbmcgZm9yIG1haWwgYm9keSwgbGlzdHMgYWRkcmVzc2VzIHRoYXQgd2VyZSBub3QgYWNjZXB0ZWQgYXMgcmVjaXBpZW50c1xuICAgIHRoaXMub25kb25lID0gKHN1Y2Nlc3MpID0+IHsgfSAvLyBUaGUgbWFpbCBoYXMgYmVlbiBzZW50LiBXYWl0IGZvciBgb25pZGxlYCBuZXh0LiBJbmRpY2F0ZXMgaWYgdGhlIG1lc3NhZ2Ugd2FzIHF1ZXVlZCBieSB0aGUgc2VydmVyLlxuICB9XG5cbiAgLyoqXG4gICAqIEluaXRpYXRlIGEgY29ubmVjdGlvbiB0byB0aGUgc2VydmVyXG4gICAqL1xuICBjb25uZWN0IChTb2NrZXRDb250cnVjdG9yID0gVENQU29ja2V0KSB7XG4gICAgdGhpcy5zb2NrZXQgPSBTb2NrZXRDb250cnVjdG9yLm9wZW4odGhpcy5ob3N0LCB0aGlzLnBvcnQsIHtcbiAgICAgIGJpbmFyeVR5cGU6ICdhcnJheWJ1ZmZlcicsXG4gICAgICB1c2VTZWN1cmVUcmFuc3BvcnQ6IHRoaXMuX3NlY3VyZU1vZGUsXG4gICAgICBjYTogdGhpcy5vcHRpb25zLmNhLFxuICAgICAgdGxzV29ya2VyUGF0aDogdGhpcy5vcHRpb25zLnRsc1dvcmtlclBhdGgsXG4gICAgICB3czogdGhpcy5vcHRpb25zLndzXG4gICAgfSlcblxuICAgIC8vIGFsbG93cyBjZXJ0aWZpY2F0ZSBoYW5kbGluZyBmb3IgcGxhdGZvcm0gdy9vIG5hdGl2ZSB0bHMgc3VwcG9ydFxuICAgIC8vIG9uY2VydCBpcyBub24gc3RhbmRhcmQgc28gc2V0dGluZyBpdCBtaWdodCB0aHJvdyBpZiB0aGUgc29ja2V0IG9iamVjdCBpcyBpbW11dGFibGVcbiAgICB0cnkge1xuICAgICAgdGhpcy5zb2NrZXQub25jZXJ0ID0gdGhpcy5vbmNlcnRcbiAgICB9IGNhdGNoIChFKSB7IH1cbiAgICB0aGlzLnNvY2tldC5vbmVycm9yID0gdGhpcy5fb25FcnJvci5iaW5kKHRoaXMpXG4gICAgdGhpcy5zb2NrZXQub25vcGVuID0gdGhpcy5fb25PcGVuLmJpbmQodGhpcylcbiAgfVxuXG4gIC8qKlxuICAgKiBQYXVzZXMgYGRhdGFgIGV2ZW50cyBmcm9tIHRoZSBkb3duc3RyZWFtIFNNVFAgc2VydmVyXG4gICAqL1xuICBzdXNwZW5kICgpIHtcbiAgICBpZiAodGhpcy5zb2NrZXQgJiYgdGhpcy5zb2NrZXQucmVhZHlTdGF0ZSA9PT0gJ29wZW4nKSB7XG4gICAgICB0aGlzLnNvY2tldC5zdXNwZW5kKClcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogUmVzdW1lcyBgZGF0YWAgZXZlbnRzIGZyb20gdGhlIGRvd25zdHJlYW0gU01UUCBzZXJ2ZXIuIEJlIGNhcmVmdWwgb2Ygbm90XG4gICAqIHJlc3VtaW5nIHNvbWV0aGluZyB0aGF0IGlzIG5vdCBzdXNwZW5kZWQgLSBhbiBlcnJvciBpcyB0aHJvd24gaW4gdGhpcyBjYXNlXG4gICAqL1xuICByZXN1bWUgKCkge1xuICAgIGlmICh0aGlzLnNvY2tldCAmJiB0aGlzLnNvY2tldC5yZWFkeVN0YXRlID09PSAnb3BlbicpIHtcbiAgICAgIHRoaXMuc29ja2V0LnJlc3VtZSgpXG4gICAgfVxuICB9XG5cbiAgLyoqXG4gICAqIFNlbmRzIFFVSVRcbiAgICovXG4gIHF1aXQgKCkge1xuICAgIHRoaXMubG9nZ2VyLmRlYnVnKERFQlVHX1RBRywgJ1NlbmRpbmcgUVVJVC4uLicpXG4gICAgdGhpcy5fc2VuZENvbW1hbmQoJ1FVSVQnKVxuICAgIHRoaXMuX2N1cnJlbnRBY3Rpb24gPSB0aGlzLmNsb3NlXG4gIH1cblxuICAvKipcbiAgICogUmVzZXQgYXV0aGVudGljYXRpb25cbiAgICpcbiAgICogQHBhcmFtIHtPYmplY3R9IFthdXRoXSBVc2UgdGhpcyBpZiB5b3Ugd2FudCB0byBhdXRoZW50aWNhdGUgYXMgYW5vdGhlciB1c2VyXG4gICAqL1xuICByZXNldCAoYXV0aCkge1xuICAgIHRoaXMub3B0aW9ucy5hdXRoID0gYXV0aCB8fCB0aGlzLm9wdGlvbnMuYXV0aFxuICAgIHRoaXMubG9nZ2VyLmRlYnVnKERFQlVHX1RBRywgJ1NlbmRpbmcgUlNFVC4uLicpXG4gICAgdGhpcy5fc2VuZENvbW1hbmQoJ1JTRVQnKVxuICAgIHRoaXMuX2N1cnJlbnRBY3Rpb24gPSB0aGlzLl9hY3Rpb25SU0VUXG4gIH1cblxuICAvKipcbiAgICogQ2xvc2VzIHRoZSBjb25uZWN0aW9uIHRvIHRoZSBzZXJ2ZXJcbiAgICovXG4gIGNsb3NlICgpIHtcbiAgICB0aGlzLmxvZ2dlci5kZWJ1ZyhERUJVR19UQUcsICdDbG9zaW5nIGNvbm5lY3Rpb24uLi4nKVxuICAgIGlmICh0aGlzLnNvY2tldCAmJiB0aGlzLnNvY2tldC5yZWFkeVN0YXRlID09PSAnb3BlbicpIHtcbiAgICAgIHRoaXMuc29ja2V0LmNsb3NlKClcbiAgICB9IGVsc2Uge1xuICAgICAgdGhpcy5fZGVzdHJveSgpXG4gICAgfVxuICB9XG5cbiAgLy8gTWFpbCByZWxhdGVkIG1ldGhvZHNcblxuICAvKipcbiAgICogSW5pdGlhdGVzIGEgbmV3IG1lc3NhZ2UgYnkgc3VibWl0dGluZyBlbnZlbG9wZSBkYXRhLCBzdGFydGluZyB3aXRoXG4gICAqIGBNQUlMIEZST006YCBjb21tYW5kLiBVc2UgYWZ0ZXIgYG9uaWRsZWAgZXZlbnRcbiAgICpcbiAgICogQHBhcmFtIHtPYmplY3R9IGVudmVsb3BlIEVudmVsb3BlIG9iamVjdCBpbiB0aGUgZm9ybSBvZiB7ZnJvbTpcIi4uLlwiLCB0bzpbXCIuLi5cIl19XG4gICAqL1xuICB1c2VFbnZlbG9wZSAoZW52ZWxvcGUpIHtcbiAgICB0aGlzLl9lbnZlbG9wZSA9IGVudmVsb3BlIHx8IHt9XG4gICAgdGhpcy5fZW52ZWxvcGUuZnJvbSA9IFtdLmNvbmNhdCh0aGlzLl9lbnZlbG9wZS5mcm9tIHx8ICgnYW5vbnltb3VzQCcgKyB0aGlzLm9wdGlvbnMubmFtZSkpWzBdXG4gICAgdGhpcy5fZW52ZWxvcGUudG8gPSBbXS5jb25jYXQodGhpcy5fZW52ZWxvcGUudG8gfHwgW10pXG5cbiAgICAvLyBjbG9uZSB0aGUgcmVjaXBpZW50cyBhcnJheSBmb3IgbGF0dGVyIG1hbmlwdWxhdGlvblxuICAgIHRoaXMuX2VudmVsb3BlLnJjcHRRdWV1ZSA9IFtdLmNvbmNhdCh0aGlzLl9lbnZlbG9wZS50bylcbiAgICB0aGlzLl9lbnZlbG9wZS5yY3B0RmFpbGVkID0gW11cbiAgICB0aGlzLl9lbnZlbG9wZS5yZXNwb25zZVF1ZXVlID0gW11cblxuICAgIHRoaXMuX2N1cnJlbnRBY3Rpb24gPSB0aGlzLl9hY3Rpb25NQUlMXG4gICAgdGhpcy5sb2dnZXIuZGVidWcoREVCVUdfVEFHLCAnU2VuZGluZyBNQUlMIEZST00uLi4nKVxuICAgIHRoaXMuX3NlbmRDb21tYW5kKCdNQUlMIEZST006PCcgKyAodGhpcy5fZW52ZWxvcGUuZnJvbSkgKyAnPicpXG4gIH1cblxuICAvKipcbiAgICogU2VuZCBBU0NJSSBkYXRhIHRvIHRoZSBzZXJ2ZXIuIFdvcmtzIG9ubHkgaW4gZGF0YSBtb2RlIChhZnRlciBgb25yZWFkeWAgZXZlbnQpLCBpZ25vcmVkXG4gICAqIG90aGVyd2lzZVxuICAgKlxuICAgKiBAcGFyYW0ge1N0cmluZ30gY2h1bmsgQVNDSUkgc3RyaW5nIChxdW90ZWQtcHJpbnRhYmxlLCBiYXNlNjQgZXRjLikgdG8gYmUgc2VudCB0byB0aGUgc2VydmVyXG4gICAqIEByZXR1cm4ge0Jvb2xlYW59IElmIHRydWUsIGl0IGlzIHNhZmUgdG8gc2VuZCBtb3JlIGRhdGEsIGlmIGZhbHNlLCB5b3UgKnNob3VsZCogd2FpdCBmb3IgdGhlIG9uZHJhaW4gZXZlbnQgYmVmb3JlIHNlbmRpbmcgbW9yZVxuICAgKi9cbiAgc2VuZCAoY2h1bmspIHtcbiAgICAvLyB3b3JrcyBvbmx5IGluIGRhdGEgbW9kZVxuICAgIGlmICghdGhpcy5fZGF0YU1vZGUpIHtcbiAgICAgIC8vIHRoaXMgbGluZSBzaG91bGQgbmV2ZXIgYmUgcmVhY2hlZCBidXQgaWYgaXQgZG9lcyxcbiAgICAgIC8vIGFjdCBsaWtlIGV2ZXJ5dGhpbmcncyBub3JtYWwuXG4gICAgICByZXR1cm4gdHJ1ZVxuICAgIH1cblxuICAgIC8vIFRPRE86IGlmIHRoZSBjaHVuayBpcyBhbiBhcnJheWJ1ZmZlciwgdXNlIGEgc2VwYXJhdGUgZnVuY3Rpb24gdG8gc2VuZCB0aGUgZGF0YVxuICAgIHJldHVybiB0aGlzLl9zZW5kU3RyaW5nKGNodW5rKVxuICB9XG5cbiAgLyoqXG4gICAqIEluZGljYXRlcyB0aGF0IGEgZGF0YSBzdHJlYW0gZm9yIHRoZSBzb2NrZXQgaXMgZW5kZWQuIFdvcmtzIG9ubHkgaW4gZGF0YVxuICAgKiBtb2RlIChhZnRlciBgb25yZWFkeWAgZXZlbnQpLCBpZ25vcmVkIG90aGVyd2lzZS4gVXNlIGl0IHdoZW4geW91IGFyZSBkb25lXG4gICAqIHdpdGggc2VuZGluZyB0aGUgbWFpbC4gVGhpcyBtZXRob2QgZG9lcyBub3QgY2xvc2UgdGhlIHNvY2tldC4gT25jZSB0aGUgbWFpbFxuICAgKiBoYXMgYmVlbiBxdWV1ZWQgYnkgdGhlIHNlcnZlciwgYG9uZG9uZWAgYW5kIGBvbmlkbGVgIGFyZSBlbWl0dGVkLlxuICAgKlxuICAgKiBAcGFyYW0ge0J1ZmZlcn0gW2NodW5rXSBDaHVuayBvZiBkYXRhIHRvIGJlIHNlbnQgdG8gdGhlIHNlcnZlclxuICAgKi9cbiAgZW5kIChjaHVuaykge1xuICAgIC8vIHdvcmtzIG9ubHkgaW4gZGF0YSBtb2RlXG4gICAgaWYgKCF0aGlzLl9kYXRhTW9kZSkge1xuICAgICAgLy8gdGhpcyBsaW5lIHNob3VsZCBuZXZlciBiZSByZWFjaGVkIGJ1dCBpZiBpdCBkb2VzLFxuICAgICAgLy8gYWN0IGxpa2UgZXZlcnl0aGluZydzIG5vcm1hbC5cbiAgICAgIHJldHVybiB0cnVlXG4gICAgfVxuXG4gICAgaWYgKGNodW5rICYmIGNodW5rLmxlbmd0aCkge1xuICAgICAgdGhpcy5zZW5kKGNodW5rKVxuICAgIH1cblxuICAgIC8vIHJlZGlyZWN0IG91dHB1dCBmcm9tIHRoZSBzZXJ2ZXIgdG8gX2FjdGlvblN0cmVhbVxuICAgIHRoaXMuX2N1cnJlbnRBY3Rpb24gPSB0aGlzLl9hY3Rpb25TdHJlYW1cblxuICAgIC8vIGluZGljYXRlIHRoYXQgdGhlIHN0cmVhbSBoYXMgZW5kZWQgYnkgc2VuZGluZyBhIHNpbmdsZSBkb3Qgb24gaXRzIG93biBsaW5lXG4gICAgLy8gaWYgdGhlIGNsaWVudCBhbHJlYWR5IGNsb3NlZCB0aGUgZGF0YSB3aXRoIFxcclxcbiBubyBuZWVkIHRvIGRvIGl0IGFnYWluXG4gICAgaWYgKHRoaXMuX2xhc3REYXRhQnl0ZXMgPT09ICdcXHJcXG4nKSB7XG4gICAgICB0aGlzLndhaXREcmFpbiA9IHRoaXMuX3NlbmQobmV3IFVpbnQ4QXJyYXkoWzB4MkUsIDB4MEQsIDB4MEFdKS5idWZmZXIpIC8vIC5cXHJcXG5cbiAgICB9IGVsc2UgaWYgKHRoaXMuX2xhc3REYXRhQnl0ZXMuc3Vic3RyKC0xKSA9PT0gJ1xccicpIHtcbiAgICAgIHRoaXMud2FpdERyYWluID0gdGhpcy5fc2VuZChuZXcgVWludDhBcnJheShbMHgwQSwgMHgyRSwgMHgwRCwgMHgwQV0pLmJ1ZmZlcikgLy8gXFxuLlxcclxcblxuICAgIH0gZWxzZSB7XG4gICAgICB0aGlzLndhaXREcmFpbiA9IHRoaXMuX3NlbmQobmV3IFVpbnQ4QXJyYXkoWzB4MEQsIDB4MEEsIDB4MkUsIDB4MEQsIDB4MEFdKS5idWZmZXIpIC8vIFxcclxcbi5cXHJcXG5cbiAgICB9XG5cbiAgICAvLyBlbmQgZGF0YSBtb2RlLCByZXNldCB0aGUgdmFyaWFibGVzIGZvciBleHRlbmRpbmcgdGhlIHRpbWVvdXQgaW4gZGF0YSBtb2RlXG4gICAgdGhpcy5fZGF0YU1vZGUgPSBmYWxzZVxuICAgIHRoaXMuX3NvY2tldFRpbWVvdXRTdGFydCA9IGZhbHNlXG4gICAgdGhpcy5fc29ja2V0VGltZW91dFBlcmlvZCA9IGZhbHNlXG5cbiAgICByZXR1cm4gdGhpcy53YWl0RHJhaW5cbiAgfVxuXG4gIC8vIFBSSVZBVEUgTUVUSE9EU1xuXG4gIC8vIEVWRU5UIEhBTkRMRVJTIEZPUiBUSEUgU09DS0VUXG5cbiAgLyoqXG4gICAqIENvbm5lY3Rpb24gbGlzdGVuZXIgdGhhdCBpcyBydW4gd2hlbiB0aGUgY29ubmVjdGlvbiB0byB0aGUgc2VydmVyIGlzIG9wZW5lZC5cbiAgICogU2V0cyB1cCBkaWZmZXJlbnQgZXZlbnQgaGFuZGxlcnMgZm9yIHRoZSBvcGVuZWQgc29ja2V0XG4gICAqXG4gICAqIEBldmVudFxuICAgKiBAcGFyYW0ge0V2ZW50fSBldnQgRXZlbnQgb2JqZWN0LiBOb3QgdXNlZFxuICAgKi9cbiAgX29uT3BlbiAoZXZlbnQpIHtcbiAgICBpZiAoZXZlbnQgJiYgZXZlbnQuZGF0YSAmJiBldmVudC5kYXRhLnByb3h5SG9zdG5hbWUpIHtcbiAgICAgIHRoaXMub3B0aW9ucy5uYW1lID0gZXZlbnQuZGF0YS5wcm94eUhvc3RuYW1lXG4gICAgfVxuXG4gICAgdGhpcy5zb2NrZXQub25kYXRhID0gdGhpcy5fb25EYXRhLmJpbmQodGhpcylcblxuICAgIHRoaXMuc29ja2V0Lm9uY2xvc2UgPSB0aGlzLl9vbkNsb3NlLmJpbmQodGhpcylcbiAgICB0aGlzLnNvY2tldC5vbmRyYWluID0gdGhpcy5fb25EcmFpbi5iaW5kKHRoaXMpXG5cbiAgICB0aGlzLl9wYXJzZXIub25kYXRhID0gdGhpcy5fb25Db21tYW5kLmJpbmQodGhpcylcblxuICAgIHRoaXMuX2N1cnJlbnRBY3Rpb24gPSB0aGlzLl9hY3Rpb25HcmVldGluZ1xuICB9XG5cbiAgLyoqXG4gICAqIERhdGEgbGlzdGVuZXIgZm9yIGNodW5rcyBvZiBkYXRhIGVtaXR0ZWQgYnkgdGhlIHNlcnZlclxuICAgKlxuICAgKiBAZXZlbnRcbiAgICogQHBhcmFtIHtFdmVudH0gZXZ0IEV2ZW50IG9iamVjdC4gU2VlIGBldnQuZGF0YWAgZm9yIHRoZSBjaHVuayByZWNlaXZlZFxuICAgKi9cbiAgX29uRGF0YSAoZXZ0KSB7XG4gICAgY2xlYXJUaW1lb3V0KHRoaXMuX3NvY2tldFRpbWVvdXRUaW1lcilcbiAgICB2YXIgc3RyaW5nUGF5bG9hZCA9IG5ldyBUZXh0RGVjb2RlcignVVRGLTgnKS5kZWNvZGUobmV3IFVpbnQ4QXJyYXkoZXZ0LmRhdGEpKVxuICAgIHRoaXMubG9nZ2VyLmRlYnVnKERFQlVHX1RBRywgJ1NFUlZFUjogJyArIHN0cmluZ1BheWxvYWQpXG4gICAgdGhpcy5fcGFyc2VyLnNlbmQoc3RyaW5nUGF5bG9hZClcbiAgfVxuXG4gIC8qKlxuICAgKiBNb3JlIGRhdGEgY2FuIGJlIGJ1ZmZlcmVkIGluIHRoZSBzb2NrZXQsIGB3YWl0RHJhaW5gIGlzIHJlc2V0IHRvIGZhbHNlXG4gICAqXG4gICAqIEBldmVudFxuICAgKiBAcGFyYW0ge0V2ZW50fSBldnQgRXZlbnQgb2JqZWN0LiBOb3QgdXNlZFxuICAgKi9cbiAgX29uRHJhaW4gKCkge1xuICAgIHRoaXMud2FpdERyYWluID0gZmFsc2VcbiAgICB0aGlzLm9uZHJhaW4oKVxuICB9XG5cbiAgLyoqXG4gICAqIEVycm9yIGhhbmRsZXIgZm9yIHRoZSBzb2NrZXRcbiAgICpcbiAgICogQGV2ZW50XG4gICAqIEBwYXJhbSB7RXZlbnR9IGV2dCBFdmVudCBvYmplY3QuIFNlZSBldnQuZGF0YSBmb3IgdGhlIGVycm9yXG4gICAqL1xuICBfb25FcnJvciAoZXZ0KSB7XG4gICAgaWYgKGV2dCBpbnN0YW5jZW9mIEVycm9yICYmIGV2dC5tZXNzYWdlKSB7XG4gICAgICB0aGlzLmxvZ2dlci5lcnJvcihERUJVR19UQUcsIGV2dClcbiAgICAgIHRoaXMub25lcnJvcihldnQpXG4gICAgfSBlbHNlIGlmIChldnQgJiYgZXZ0LmRhdGEgaW5zdGFuY2VvZiBFcnJvcikge1xuICAgICAgdGhpcy5sb2dnZXIuZXJyb3IoREVCVUdfVEFHLCBldnQuZGF0YSlcbiAgICAgIHRoaXMub25lcnJvcihldnQuZGF0YSlcbiAgICB9IGVsc2Uge1xuICAgICAgdGhpcy5sb2dnZXIuZXJyb3IoREVCVUdfVEFHLCBuZXcgRXJyb3IoKGV2dCAmJiBldnQuZGF0YSAmJiBldnQuZGF0YS5tZXNzYWdlKSB8fCBldnQuZGF0YSB8fCBldnQgfHwgJ0Vycm9yJykpXG4gICAgICB0aGlzLm9uZXJyb3IobmV3IEVycm9yKChldnQgJiYgZXZ0LmRhdGEgJiYgZXZ0LmRhdGEubWVzc2FnZSkgfHwgZXZ0LmRhdGEgfHwgZXZ0IHx8ICdFcnJvcicpKVxuICAgIH1cblxuICAgIHRoaXMuY2xvc2UoKVxuICB9XG5cbiAgLyoqXG4gICAqIEluZGljYXRlcyB0aGF0IHRoZSBzb2NrZXQgaGFzIGJlZW4gY2xvc2VkXG4gICAqXG4gICAqIEBldmVudFxuICAgKiBAcGFyYW0ge0V2ZW50fSBldnQgRXZlbnQgb2JqZWN0LiBOb3QgdXNlZFxuICAgKi9cbiAgX29uQ2xvc2UgKCkge1xuICAgIHRoaXMubG9nZ2VyLmRlYnVnKERFQlVHX1RBRywgJ1NvY2tldCBjbG9zZWQuJylcbiAgICB0aGlzLl9kZXN0cm95KClcbiAgfVxuXG4gIC8qKlxuICAgKiBUaGlzIGlzIG5vdCBhIHNvY2tldCBkYXRhIGhhbmRsZXIgYnV0IHRoZSBoYW5kbGVyIGZvciBkYXRhIGVtaXR0ZWQgYnkgdGhlIHBhcnNlcixcbiAgICogc28gdGhpcyBkYXRhIGlzIHNhZmUgdG8gdXNlIGFzIGl0IGlzIGFsd2F5cyBjb21wbGV0ZSAoc2VydmVyIG1pZ2h0IHNlbmQgcGFydGlhbCBjaHVua3MpXG4gICAqXG4gICAqIEBldmVudFxuICAgKiBAcGFyYW0ge09iamVjdH0gY29tbWFuZCBQYXJzZWQgZGF0YVxuICAgKi9cbiAgX29uQ29tbWFuZCAoY29tbWFuZCkge1xuICAgIGlmICh0eXBlb2YgdGhpcy5fY3VycmVudEFjdGlvbiA9PT0gJ2Z1bmN0aW9uJykge1xuICAgICAgdGhpcy5fY3VycmVudEFjdGlvbihjb21tYW5kKVxuICAgIH1cbiAgfVxuXG4gIF9vblRpbWVvdXQgKCkge1xuICAgIC8vIGluZm9ybSBhYm91dCB0aGUgdGltZW91dCBhbmQgc2h1dCBkb3duXG4gICAgdmFyIGVycm9yID0gbmV3IEVycm9yKCdTb2NrZXQgdGltZWQgb3V0IScpXG4gICAgdGhpcy5fb25FcnJvcihlcnJvcilcbiAgfVxuXG4gIC8qKlxuICAgKiBFbnN1cmVzIHRoYXQgdGhlIGNvbm5lY3Rpb24gaXMgY2xvc2VkIGFuZCBzdWNoXG4gICAqL1xuICBfZGVzdHJveSAoKSB7XG4gICAgY2xlYXJUaW1lb3V0KHRoaXMuX3NvY2tldFRpbWVvdXRUaW1lcilcblxuICAgIGlmICghdGhpcy5kZXN0cm95ZWQpIHtcbiAgICAgIHRoaXMuZGVzdHJveWVkID0gdHJ1ZVxuICAgICAgdGhpcy5vbmNsb3NlKClcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogU2VuZHMgYSBzdHJpbmcgdG8gdGhlIHNvY2tldC5cbiAgICpcbiAgICogQHBhcmFtIHtTdHJpbmd9IGNodW5rIEFTQ0lJIHN0cmluZyAocXVvdGVkLXByaW50YWJsZSwgYmFzZTY0IGV0Yy4pIHRvIGJlIHNlbnQgdG8gdGhlIHNlcnZlclxuICAgKiBAcmV0dXJuIHtCb29sZWFufSBJZiB0cnVlLCBpdCBpcyBzYWZlIHRvIHNlbmQgbW9yZSBkYXRhLCBpZiBmYWxzZSwgeW91ICpzaG91bGQqIHdhaXQgZm9yIHRoZSBvbmRyYWluIGV2ZW50IGJlZm9yZSBzZW5kaW5nIG1vcmVcbiAgICovXG4gIF9zZW5kU3RyaW5nIChjaHVuaykge1xuICAgIC8vIGVzY2FwZSBkb3RzXG4gICAgaWYgKCF0aGlzLm9wdGlvbnMuZGlzYWJsZUVzY2FwaW5nKSB7XG4gICAgICBjaHVuayA9IGNodW5rLnJlcGxhY2UoL1xcblxcLi9nLCAnXFxuLi4nKVxuICAgICAgaWYgKCh0aGlzLl9sYXN0RGF0YUJ5dGVzLnN1YnN0cigtMSkgPT09ICdcXG4nIHx8ICF0aGlzLl9sYXN0RGF0YUJ5dGVzKSAmJiBjaHVuay5jaGFyQXQoMCkgPT09ICcuJykge1xuICAgICAgICBjaHVuayA9ICcuJyArIGNodW5rXG4gICAgICB9XG4gICAgfVxuXG4gICAgLy8gS2VlcGluZyBleWUgb24gdGhlIGxhc3QgYnl0ZXMgc2VudCwgdG8gc2VlIGlmIHRoZXJlIGlzIGEgPENSPjxMRj4gc2VxdWVuY2VcbiAgICAvLyBhdCB0aGUgZW5kIHdoaWNoIGlzIG5lZWRlZCB0byBlbmQgdGhlIGRhdGEgc3RyZWFtXG4gICAgaWYgKGNodW5rLmxlbmd0aCA+IDIpIHtcbiAgICAgIHRoaXMuX2xhc3REYXRhQnl0ZXMgPSBjaHVuay5zdWJzdHIoLTIpXG4gICAgfSBlbHNlIGlmIChjaHVuay5sZW5ndGggPT09IDEpIHtcbiAgICAgIHRoaXMuX2xhc3REYXRhQnl0ZXMgPSB0aGlzLl9sYXN0RGF0YUJ5dGVzLnN1YnN0cigtMSkgKyBjaHVua1xuICAgIH1cblxuICAgIHRoaXMubG9nZ2VyLmRlYnVnKERFQlVHX1RBRywgJ1NlbmRpbmcgJyArIGNodW5rLmxlbmd0aCArICcgYnl0ZXMgb2YgcGF5bG9hZCcpXG5cbiAgICAvLyBwYXNzIHRoZSBjaHVuayB0byB0aGUgc29ja2V0XG4gICAgdGhpcy53YWl0RHJhaW4gPSB0aGlzLl9zZW5kKG5ldyBUZXh0RW5jb2RlcignVVRGLTgnKS5lbmNvZGUoY2h1bmspLmJ1ZmZlcilcbiAgICByZXR1cm4gdGhpcy53YWl0RHJhaW5cbiAgfVxuXG4gIC8qKlxuICAgKiBTZW5kIGEgc3RyaW5nIGNvbW1hbmQgdG8gdGhlIHNlcnZlciwgYWxzbyBhcHBlbmQgXFxyXFxuIGlmIG5lZWRlZFxuICAgKlxuICAgKiBAcGFyYW0ge1N0cmluZ30gc3RyIFN0cmluZyB0byBiZSBzZW50IHRvIHRoZSBzZXJ2ZXJcbiAgICovXG4gIF9zZW5kQ29tbWFuZCAoc3RyKSB7XG4gICAgdGhpcy53YWl0RHJhaW4gPSB0aGlzLl9zZW5kKG5ldyBUZXh0RW5jb2RlcignVVRGLTgnKS5lbmNvZGUoc3RyICsgKHN0ci5zdWJzdHIoLTIpICE9PSAnXFxyXFxuJyA/ICdcXHJcXG4nIDogJycpKS5idWZmZXIpXG4gIH1cblxuICBfc2VuZCAoYnVmZmVyKSB7XG4gICAgdGhpcy5fc2V0VGltZW91dChidWZmZXIuYnl0ZUxlbmd0aClcbiAgICByZXR1cm4gdGhpcy5zb2NrZXQuc2VuZChidWZmZXIpXG4gIH1cblxuICBfc2V0VGltZW91dCAoYnl0ZUxlbmd0aCkge1xuICAgIHZhciBwcm9sb25nUGVyaW9kID0gTWF0aC5mbG9vcihieXRlTGVuZ3RoICogdGhpcy50aW1lb3V0U29ja2V0TXVsdGlwbGllcilcbiAgICB2YXIgdGltZW91dFxuXG4gICAgaWYgKHRoaXMuX2RhdGFNb2RlKSB7XG4gICAgICAvLyB3ZSdyZSBpbiBkYXRhIG1vZGUsIHNvIHdlIGNvdW50IG9ubHkgb25lIHRpbWVvdXQgdGhhdCBnZXQgZXh0ZW5kZWQgZm9yIGV2ZXJ5IHNlbmQoKS5cbiAgICAgIHZhciBub3cgPSBEYXRlLm5vdygpXG5cbiAgICAgIC8vIHRoZSBvbGQgdGltZW91dCBzdGFydCB0aW1lXG4gICAgICB0aGlzLl9zb2NrZXRUaW1lb3V0U3RhcnQgPSB0aGlzLl9zb2NrZXRUaW1lb3V0U3RhcnQgfHwgbm93XG5cbiAgICAgIC8vIHRoZSBvbGQgdGltZW91dCBwZXJpb2QsIG5vcm1hbGl6ZWQgdG8gYSBtaW5pbXVtIG9mIFRJTUVPVVRfU09DS0VUX0xPV0VSX0JPVU5EXG4gICAgICB0aGlzLl9zb2NrZXRUaW1lb3V0UGVyaW9kID0gKHRoaXMuX3NvY2tldFRpbWVvdXRQZXJpb2QgfHwgdGhpcy50aW1lb3V0U29ja2V0TG93ZXJCb3VuZCkgKyBwcm9sb25nUGVyaW9kXG5cbiAgICAgIC8vIHRoZSBuZXcgdGltZW91dCBpcyB0aGUgZGVsdGEgYmV0d2VlbiB0aGUgbmV3IGZpcmluZyB0aW1lICg9IHRpbWVvdXQgcGVyaW9kICsgdGltZW91dCBzdGFydCB0aW1lKSBhbmQgbm93XG4gICAgICB0aW1lb3V0ID0gdGhpcy5fc29ja2V0VGltZW91dFN0YXJ0ICsgdGhpcy5fc29ja2V0VGltZW91dFBlcmlvZCAtIG5vd1xuICAgIH0gZWxzZSB7XG4gICAgICAvLyBzZXQgbmV3IHRpbW91dFxuICAgICAgdGltZW91dCA9IHRoaXMudGltZW91dFNvY2tldExvd2VyQm91bmQgKyBwcm9sb25nUGVyaW9kXG4gICAgfVxuXG4gICAgY2xlYXJUaW1lb3V0KHRoaXMuX3NvY2tldFRpbWVvdXRUaW1lcikgLy8gY2xlYXIgcGVuZGluZyB0aW1lb3V0c1xuICAgIHRoaXMuX3NvY2tldFRpbWVvdXRUaW1lciA9IHNldFRpbWVvdXQodGhpcy5fb25UaW1lb3V0LmJpbmQodGhpcyksIHRpbWVvdXQpIC8vIGFybSB0aGUgbmV4dCB0aW1lb3V0XG4gIH1cblxuICAvKipcbiAgICogSW50aXRpYXRlIGF1dGhlbnRpY2F0aW9uIHNlcXVlbmNlIGlmIG5lZWRlZFxuICAgKi9cbiAgX2F1dGhlbnRpY2F0ZVVzZXIgKCkge1xuICAgIGlmICghdGhpcy5vcHRpb25zLmF1dGgpIHtcbiAgICAgIC8vIG5vIG5lZWQgdG8gYXV0aGVudGljYXRlLCBhdCBsZWFzdCBubyBkYXRhIGdpdmVuXG4gICAgICB0aGlzLl9jdXJyZW50QWN0aW9uID0gdGhpcy5fYWN0aW9uSWRsZVxuICAgICAgdGhpcy5vbmlkbGUoKSAvLyByZWFkeSB0byB0YWtlIG9yZGVyc1xuICAgICAgcmV0dXJuXG4gICAgfVxuXG4gICAgdmFyIGF1dGhcblxuICAgIGlmICghdGhpcy5vcHRpb25zLmF1dGhNZXRob2QgJiYgdGhpcy5vcHRpb25zLmF1dGgueG9hdXRoMikge1xuICAgICAgdGhpcy5vcHRpb25zLmF1dGhNZXRob2QgPSAnWE9BVVRIMidcbiAgICB9XG5cbiAgICBpZiAodGhpcy5vcHRpb25zLmF1dGhNZXRob2QpIHtcbiAgICAgIGF1dGggPSB0aGlzLm9wdGlvbnMuYXV0aE1ldGhvZC50b1VwcGVyQ2FzZSgpLnRyaW0oKVxuICAgIH0gZWxzZSB7XG4gICAgICAvLyB1c2UgZmlyc3Qgc3VwcG9ydGVkXG4gICAgICBhdXRoID0gKHRoaXMuX3N1cHBvcnRlZEF1dGhbMF0gfHwgJ1BMQUlOJykudG9VcHBlckNhc2UoKS50cmltKClcbiAgICB9XG5cbiAgICBzd2l0Y2ggKGF1dGgpIHtcbiAgICAgIGNhc2UgJ0xPR0lOJzpcbiAgICAgICAgLy8gTE9HSU4gaXMgYSAzIHN0ZXAgYXV0aGVudGljYXRpb24gcHJvY2Vzc1xuICAgICAgICAvLyBDOiBBVVRIIExPR0lOXG4gICAgICAgIC8vIEM6IEJBU0U2NChVU0VSKVxuICAgICAgICAvLyBDOiBCQVNFNjQoUEFTUylcbiAgICAgICAgdGhpcy5sb2dnZXIuZGVidWcoREVCVUdfVEFHLCAnQXV0aGVudGljYXRpb24gdmlhIEFVVEggTE9HSU4nKVxuICAgICAgICB0aGlzLl9jdXJyZW50QWN0aW9uID0gdGhpcy5fYWN0aW9uQVVUSF9MT0dJTl9VU0VSXG4gICAgICAgIHRoaXMuX3NlbmRDb21tYW5kKCdBVVRIIExPR0lOJylcbiAgICAgICAgcmV0dXJuXG4gICAgICBjYXNlICdQTEFJTic6XG4gICAgICAgIC8vIEFVVEggUExBSU4gaXMgYSAxIHN0ZXAgYXV0aGVudGljYXRpb24gcHJvY2Vzc1xuICAgICAgICAvLyBDOiBBVVRIIFBMQUlOIEJBU0U2NChcXDAgVVNFUiBcXDAgUEFTUylcbiAgICAgICAgdGhpcy5sb2dnZXIuZGVidWcoREVCVUdfVEFHLCAnQXV0aGVudGljYXRpb24gdmlhIEFVVEggUExBSU4nKVxuICAgICAgICB0aGlzLl9jdXJyZW50QWN0aW9uID0gdGhpcy5fYWN0aW9uQVVUSENvbXBsZXRlXG4gICAgICAgIHRoaXMuX3NlbmRDb21tYW5kKFxuICAgICAgICAgIC8vIGNvbnZlcnQgdG8gQkFTRTY0XG4gICAgICAgICAgJ0FVVEggUExBSU4gJyArXG4gICAgICAgICAgZW5jb2RlKFxuICAgICAgICAgICAgLy8gdGhpcy5vcHRpb25zLmF1dGgudXNlcisnXFx1MDAwMCcrXG4gICAgICAgICAgICAnXFx1MDAwMCcgKyAvLyBza2lwIGF1dGhvcml6YXRpb24gaWRlbnRpdHkgYXMgaXQgY2F1c2VzIHByb2JsZW1zIHdpdGggc29tZSBzZXJ2ZXJzXG4gICAgICAgICAgICB0aGlzLm9wdGlvbnMuYXV0aC51c2VyICsgJ1xcdTAwMDAnICtcbiAgICAgICAgICAgIHRoaXMub3B0aW9ucy5hdXRoLnBhc3MpXG4gICAgICAgIClcbiAgICAgICAgcmV0dXJuXG4gICAgICBjYXNlICdYT0FVVEgyJzpcbiAgICAgICAgLy8gU2VlIGh0dHBzOi8vZGV2ZWxvcGVycy5nb29nbGUuY29tL2dtYWlsL3hvYXV0aDJfcHJvdG9jb2wjc210cF9wcm90b2NvbF9leGNoYW5nZVxuICAgICAgICB0aGlzLmxvZ2dlci5kZWJ1ZyhERUJVR19UQUcsICdBdXRoZW50aWNhdGlvbiB2aWEgQVVUSCBYT0FVVEgyJylcbiAgICAgICAgdGhpcy5fY3VycmVudEFjdGlvbiA9IHRoaXMuX2FjdGlvbkFVVEhfWE9BVVRIMlxuICAgICAgICB0aGlzLl9zZW5kQ29tbWFuZCgnQVVUSCBYT0FVVEgyICcgKyB0aGlzLl9idWlsZFhPQXV0aDJUb2tlbih0aGlzLm9wdGlvbnMuYXV0aC51c2VyLCB0aGlzLm9wdGlvbnMuYXV0aC54b2F1dGgyKSlcbiAgICAgICAgcmV0dXJuXG4gICAgfVxuXG4gICAgdGhpcy5fb25FcnJvcihuZXcgRXJyb3IoJ1Vua25vd24gYXV0aGVudGljYXRpb24gbWV0aG9kICcgKyBhdXRoKSlcbiAgfVxuXG4gIC8vIEFDVElPTlMgRk9SIFJFU1BPTlNFUyBGUk9NIFRIRSBTTVRQIFNFUlZFUlxuXG4gIC8qKlxuICAgKiBJbml0aWFsIHJlc3BvbnNlIGZyb20gdGhlIHNlcnZlciwgbXVzdCBoYXZlIGEgc3RhdHVzIDIyMFxuICAgKlxuICAgKiBAcGFyYW0ge09iamVjdH0gY29tbWFuZCBQYXJzZWQgY29tbWFuZCBmcm9tIHRoZSBzZXJ2ZXIge3N0YXR1c0NvZGUsIGRhdGEsIGxpbmV9XG4gICAqL1xuICBfYWN0aW9uR3JlZXRpbmcgKGNvbW1hbmQpIHtcbiAgICBpZiAoY29tbWFuZC5zdGF0dXNDb2RlICE9PSAyMjApIHtcbiAgICAgIHRoaXMuX29uRXJyb3IobmV3IEVycm9yKCdJbnZhbGlkIGdyZWV0aW5nOiAnICsgY29tbWFuZC5kYXRhKSlcbiAgICAgIHJldHVyblxuICAgIH1cblxuICAgIGlmICh0aGlzLm9wdGlvbnMubG10cCkge1xuICAgICAgdGhpcy5sb2dnZXIuZGVidWcoREVCVUdfVEFHLCAnU2VuZGluZyBMSExPICcgKyB0aGlzLm9wdGlvbnMubmFtZSlcblxuICAgICAgdGhpcy5fY3VycmVudEFjdGlvbiA9IHRoaXMuX2FjdGlvbkxITE9cbiAgICAgIHRoaXMuX3NlbmRDb21tYW5kKCdMSExPICcgKyB0aGlzLm9wdGlvbnMubmFtZSlcbiAgICB9IGVsc2Uge1xuICAgICAgdGhpcy5sb2dnZXIuZGVidWcoREVCVUdfVEFHLCAnU2VuZGluZyBFSExPICcgKyB0aGlzLm9wdGlvbnMubmFtZSlcblxuICAgICAgdGhpcy5fY3VycmVudEFjdGlvbiA9IHRoaXMuX2FjdGlvbkVITE9cbiAgICAgIHRoaXMuX3NlbmRDb21tYW5kKCdFSExPICcgKyB0aGlzLm9wdGlvbnMubmFtZSlcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogUmVzcG9uc2UgdG8gTEhMT1xuICAgKlxuICAgKiBAcGFyYW0ge09iamVjdH0gY29tbWFuZCBQYXJzZWQgY29tbWFuZCBmcm9tIHRoZSBzZXJ2ZXIge3N0YXR1c0NvZGUsIGRhdGEsIGxpbmV9XG4gICAqL1xuICBfYWN0aW9uTEhMTyAoY29tbWFuZCkge1xuICAgIGlmICghY29tbWFuZC5zdWNjZXNzKSB7XG4gICAgICB0aGlzLmxvZ2dlci5lcnJvcihERUJVR19UQUcsICdMSExPIG5vdCBzdWNjZXNzZnVsJylcbiAgICAgIHRoaXMuX29uRXJyb3IobmV3IEVycm9yKGNvbW1hbmQuZGF0YSkpXG4gICAgICByZXR1cm5cbiAgICB9XG5cbiAgICAvLyBQcm9jZXNzIGFzIEVITE8gcmVzcG9uc2VcbiAgICB0aGlzLl9hY3Rpb25FSExPKGNvbW1hbmQpXG4gIH1cblxuICAvKipcbiAgICogUmVzcG9uc2UgdG8gRUhMTy4gSWYgdGhlIHJlc3BvbnNlIGlzIGFuIGVycm9yLCB0cnkgSEVMTyBpbnN0ZWFkXG4gICAqXG4gICAqIEBwYXJhbSB7T2JqZWN0fSBjb21tYW5kIFBhcnNlZCBjb21tYW5kIGZyb20gdGhlIHNlcnZlciB7c3RhdHVzQ29kZSwgZGF0YSwgbGluZX1cbiAgICovXG4gIF9hY3Rpb25FSExPIChjb21tYW5kKSB7XG4gICAgdmFyIG1hdGNoXG5cbiAgICBpZiAoIWNvbW1hbmQuc3VjY2Vzcykge1xuICAgICAgaWYgKCF0aGlzLl9zZWN1cmVNb2RlICYmIHRoaXMub3B0aW9ucy5yZXF1aXJlVExTKSB7XG4gICAgICAgIHZhciBlcnJNc2cgPSAnU1RBUlRUTFMgbm90IHN1cHBvcnRlZCB3aXRob3V0IEVITE8nXG4gICAgICAgIHRoaXMubG9nZ2VyLmVycm9yKERFQlVHX1RBRywgZXJyTXNnKVxuICAgICAgICB0aGlzLl9vbkVycm9yKG5ldyBFcnJvcihlcnJNc2cpKVxuICAgICAgICByZXR1cm5cbiAgICAgIH1cblxuICAgICAgLy8gVHJ5IEhFTE8gaW5zdGVhZFxuICAgICAgdGhpcy5sb2dnZXIud2FybihERUJVR19UQUcsICdFSExPIG5vdCBzdWNjZXNzZnVsLCB0cnlpbmcgSEVMTyAnICsgdGhpcy5vcHRpb25zLm5hbWUpXG4gICAgICB0aGlzLl9jdXJyZW50QWN0aW9uID0gdGhpcy5fYWN0aW9uSEVMT1xuICAgICAgdGhpcy5fc2VuZENvbW1hbmQoJ0hFTE8gJyArIHRoaXMub3B0aW9ucy5uYW1lKVxuICAgICAgcmV0dXJuXG4gICAgfVxuXG4gICAgLy8gRGV0ZWN0IGlmIHRoZSBzZXJ2ZXIgc3VwcG9ydHMgUExBSU4gYXV0aFxuICAgIGlmIChjb21tYW5kLmxpbmUubWF0Y2goL0FVVEgoPzpcXHMrW15cXG5dKlxccyt8XFxzKylQTEFJTi9pKSkge1xuICAgICAgdGhpcy5sb2dnZXIuZGVidWcoREVCVUdfVEFHLCAnU2VydmVyIHN1cHBvcnRzIEFVVEggUExBSU4nKVxuICAgICAgdGhpcy5fc3VwcG9ydGVkQXV0aC5wdXNoKCdQTEFJTicpXG4gICAgfVxuXG4gICAgLy8gRGV0ZWN0IGlmIHRoZSBzZXJ2ZXIgc3VwcG9ydHMgTE9HSU4gYXV0aFxuICAgIGlmIChjb21tYW5kLmxpbmUubWF0Y2goL0FVVEgoPzpcXHMrW15cXG5dKlxccyt8XFxzKylMT0dJTi9pKSkge1xuICAgICAgdGhpcy5sb2dnZXIuZGVidWcoREVCVUdfVEFHLCAnU2VydmVyIHN1cHBvcnRzIEFVVEggTE9HSU4nKVxuICAgICAgdGhpcy5fc3VwcG9ydGVkQXV0aC5wdXNoKCdMT0dJTicpXG4gICAgfVxuXG4gICAgLy8gRGV0ZWN0IGlmIHRoZSBzZXJ2ZXIgc3VwcG9ydHMgWE9BVVRIMiBhdXRoXG4gICAgaWYgKGNvbW1hbmQubGluZS5tYXRjaCgvQVVUSCg/OlxccytbXlxcbl0qXFxzK3xcXHMrKVhPQVVUSDIvaSkpIHtcbiAgICAgIHRoaXMubG9nZ2VyLmRlYnVnKERFQlVHX1RBRywgJ1NlcnZlciBzdXBwb3J0cyBBVVRIIFhPQVVUSDInKVxuICAgICAgdGhpcy5fc3VwcG9ydGVkQXV0aC5wdXNoKCdYT0FVVEgyJylcbiAgICB9XG5cbiAgICAvLyBEZXRlY3QgbWF4aW11bSBhbGxvd2VkIG1lc3NhZ2Ugc2l6ZVxuICAgIGlmICgobWF0Y2ggPSBjb21tYW5kLmxpbmUubWF0Y2goL1NJWkUgKFxcZCspL2kpKSAmJiBOdW1iZXIobWF0Y2hbMV0pKSB7XG4gICAgICBjb25zdCBtYXhBbGxvd2VkU2l6ZSA9IE51bWJlcihtYXRjaFsxXSlcbiAgICAgIHRoaXMubG9nZ2VyLmRlYnVnKERFQlVHX1RBRywgJ01heGltdW0gYWxsb3dkIG1lc3NhZ2Ugc2l6ZTogJyArIG1heEFsbG93ZWRTaXplKVxuICAgIH1cblxuICAgIC8vIERldGVjdCBpZiB0aGUgc2VydmVyIHN1cHBvcnRzIFNUQVJUVExTXG4gICAgaWYgKCF0aGlzLl9zZWN1cmVNb2RlKSB7XG4gICAgICBpZiAoKGNvbW1hbmQubGluZS5tYXRjaCgvWyAtXVNUQVJUVExTXFxzPyQvbWkpICYmICF0aGlzLm9wdGlvbnMuaWdub3JlVExTKSB8fCAhIXRoaXMub3B0aW9ucy5yZXF1aXJlVExTKSB7XG4gICAgICAgIHRoaXMuX2N1cnJlbnRBY3Rpb24gPSB0aGlzLl9hY3Rpb25TVEFSVFRMU1xuICAgICAgICB0aGlzLmxvZ2dlci5kZWJ1ZyhERUJVR19UQUcsICdTZW5kaW5nIFNUQVJUVExTJylcbiAgICAgICAgdGhpcy5fc2VuZENvbW1hbmQoJ1NUQVJUVExTJylcbiAgICAgICAgcmV0dXJuXG4gICAgICB9XG4gICAgfVxuXG4gICAgdGhpcy5fYXV0aGVudGljYXRlVXNlcigpXG4gIH1cblxuICAvKipcbiAgICogSGFuZGxlcyBzZXJ2ZXIgcmVzcG9uc2UgZm9yIFNUQVJUVExTIGNvbW1hbmQuIElmIHRoZXJlJ3MgYW4gZXJyb3JcbiAgICogdHJ5IEhFTE8gaW5zdGVhZCwgb3RoZXJ3aXNlIGluaXRpYXRlIFRMUyB1cGdyYWRlLiBJZiB0aGUgdXBncmFkZVxuICAgKiBzdWNjZWVkZXMgcmVzdGFydCB0aGUgRUhMT1xuICAgKlxuICAgKiBAcGFyYW0ge1N0cmluZ30gc3RyIE1lc3NhZ2UgZnJvbSB0aGUgc2VydmVyXG4gICAqL1xuICBfYWN0aW9uU1RBUlRUTFMgKGNvbW1hbmQpIHtcbiAgICBpZiAoIWNvbW1hbmQuc3VjY2Vzcykge1xuICAgICAgdGhpcy5sb2dnZXIuZXJyb3IoREVCVUdfVEFHLCAnU1RBUlRUTFMgbm90IHN1Y2Nlc3NmdWwnKVxuICAgICAgdGhpcy5fb25FcnJvcihuZXcgRXJyb3IoY29tbWFuZC5kYXRhKSlcbiAgICAgIHJldHVyblxuICAgIH1cblxuICAgIHRoaXMuX3NlY3VyZU1vZGUgPSB0cnVlXG4gICAgdGhpcy5zb2NrZXQudXBncmFkZVRvU2VjdXJlKClcblxuICAgIC8vIHJlc3RhcnQgcHJvdG9jb2wgZmxvd1xuICAgIHRoaXMuX2N1cnJlbnRBY3Rpb24gPSB0aGlzLl9hY3Rpb25FSExPXG4gICAgdGhpcy5fc2VuZENvbW1hbmQoJ0VITE8gJyArIHRoaXMub3B0aW9ucy5uYW1lKVxuICB9XG5cbiAgLyoqXG4gICAqIFJlc3BvbnNlIHRvIEhFTE9cbiAgICpcbiAgICogQHBhcmFtIHtPYmplY3R9IGNvbW1hbmQgUGFyc2VkIGNvbW1hbmQgZnJvbSB0aGUgc2VydmVyIHtzdGF0dXNDb2RlLCBkYXRhLCBsaW5lfVxuICAgKi9cbiAgX2FjdGlvbkhFTE8gKGNvbW1hbmQpIHtcbiAgICBpZiAoIWNvbW1hbmQuc3VjY2Vzcykge1xuICAgICAgdGhpcy5sb2dnZXIuZXJyb3IoREVCVUdfVEFHLCAnSEVMTyBub3Qgc3VjY2Vzc2Z1bCcpXG4gICAgICB0aGlzLl9vbkVycm9yKG5ldyBFcnJvcihjb21tYW5kLmRhdGEpKVxuICAgICAgcmV0dXJuXG4gICAgfVxuICAgIHRoaXMuX2F1dGhlbnRpY2F0ZVVzZXIoKVxuICB9XG5cbiAgLyoqXG4gICAqIFJlc3BvbnNlIHRvIEFVVEggTE9HSU4sIGlmIHN1Y2Nlc3NmdWwgZXhwZWN0cyBiYXNlNjQgZW5jb2RlZCB1c2VybmFtZVxuICAgKlxuICAgKiBAcGFyYW0ge09iamVjdH0gY29tbWFuZCBQYXJzZWQgY29tbWFuZCBmcm9tIHRoZSBzZXJ2ZXIge3N0YXR1c0NvZGUsIGRhdGEsIGxpbmV9XG4gICAqL1xuICBfYWN0aW9uQVVUSF9MT0dJTl9VU0VSIChjb21tYW5kKSB7XG4gICAgaWYgKGNvbW1hbmQuc3RhdHVzQ29kZSAhPT0gMzM0IHx8IGNvbW1hbmQuZGF0YSAhPT0gJ1ZYTmxjbTVoYldVNicpIHtcbiAgICAgIHRoaXMubG9nZ2VyLmVycm9yKERFQlVHX1RBRywgJ0FVVEggTE9HSU4gVVNFUiBub3Qgc3VjY2Vzc2Z1bDogJyArIGNvbW1hbmQuZGF0YSlcbiAgICAgIHRoaXMuX29uRXJyb3IobmV3IEVycm9yKCdJbnZhbGlkIGxvZ2luIHNlcXVlbmNlIHdoaWxlIHdhaXRpbmcgZm9yIFwiMzM0IFZYTmxjbTVoYldVNiBcIjogJyArIGNvbW1hbmQuZGF0YSkpXG4gICAgICByZXR1cm5cbiAgICB9XG4gICAgdGhpcy5sb2dnZXIuZGVidWcoREVCVUdfVEFHLCAnQVVUSCBMT0dJTiBVU0VSIHN1Y2Nlc3NmdWwnKVxuICAgIHRoaXMuX2N1cnJlbnRBY3Rpb24gPSB0aGlzLl9hY3Rpb25BVVRIX0xPR0lOX1BBU1NcbiAgICB0aGlzLl9zZW5kQ29tbWFuZChlbmNvZGUodGhpcy5vcHRpb25zLmF1dGgudXNlcikpXG4gIH1cblxuICAvKipcbiAgICogUmVzcG9uc2UgdG8gQVVUSCBMT0dJTiB1c2VybmFtZSwgaWYgc3VjY2Vzc2Z1bCBleHBlY3RzIGJhc2U2NCBlbmNvZGVkIHBhc3N3b3JkXG4gICAqXG4gICAqIEBwYXJhbSB7T2JqZWN0fSBjb21tYW5kIFBhcnNlZCBjb21tYW5kIGZyb20gdGhlIHNlcnZlciB7c3RhdHVzQ29kZSwgZGF0YSwgbGluZX1cbiAgICovXG4gIF9hY3Rpb25BVVRIX0xPR0lOX1BBU1MgKGNvbW1hbmQpIHtcbiAgICBpZiAoY29tbWFuZC5zdGF0dXNDb2RlICE9PSAzMzQgfHwgY29tbWFuZC5kYXRhICE9PSAnVUdGemMzZHZjbVE2Jykge1xuICAgICAgdGhpcy5sb2dnZXIuZXJyb3IoREVCVUdfVEFHLCAnQVVUSCBMT0dJTiBQQVNTIG5vdCBzdWNjZXNzZnVsOiAnICsgY29tbWFuZC5kYXRhKVxuICAgICAgdGhpcy5fb25FcnJvcihuZXcgRXJyb3IoJ0ludmFsaWQgbG9naW4gc2VxdWVuY2Ugd2hpbGUgd2FpdGluZyBmb3IgXCIzMzQgVUdGemMzZHZjbVE2IFwiOiAnICsgY29tbWFuZC5kYXRhKSlcbiAgICAgIHJldHVyblxuICAgIH1cbiAgICB0aGlzLmxvZ2dlci5kZWJ1ZyhERUJVR19UQUcsICdBVVRIIExPR0lOIFBBU1Mgc3VjY2Vzc2Z1bCcpXG4gICAgdGhpcy5fY3VycmVudEFjdGlvbiA9IHRoaXMuX2FjdGlvbkFVVEhDb21wbGV0ZVxuICAgIHRoaXMuX3NlbmRDb21tYW5kKGVuY29kZSh0aGlzLm9wdGlvbnMuYXV0aC5wYXNzKSlcbiAgfVxuXG4gIC8qKlxuICAgKiBSZXNwb25zZSB0byBBVVRIIFhPQVVUSDIgdG9rZW4sIGlmIGVycm9yIG9jY3VycyBzZW5kIGVtcHR5IHJlc3BvbnNlXG4gICAqXG4gICAqIEBwYXJhbSB7T2JqZWN0fSBjb21tYW5kIFBhcnNlZCBjb21tYW5kIGZyb20gdGhlIHNlcnZlciB7c3RhdHVzQ29kZSwgZGF0YSwgbGluZX1cbiAgICovXG4gIF9hY3Rpb25BVVRIX1hPQVVUSDIgKGNvbW1hbmQpIHtcbiAgICBpZiAoIWNvbW1hbmQuc3VjY2Vzcykge1xuICAgICAgdGhpcy5sb2dnZXIud2FybihERUJVR19UQUcsICdFcnJvciBkdXJpbmcgQVVUSCBYT0FVVEgyLCBzZW5kaW5nIGVtcHR5IHJlc3BvbnNlJylcbiAgICAgIHRoaXMuX3NlbmRDb21tYW5kKCcnKVxuICAgICAgdGhpcy5fY3VycmVudEFjdGlvbiA9IHRoaXMuX2FjdGlvbkFVVEhDb21wbGV0ZVxuICAgIH0gZWxzZSB7XG4gICAgICB0aGlzLl9hY3Rpb25BVVRIQ29tcGxldGUoY29tbWFuZClcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogQ2hlY2tzIGlmIGF1dGhlbnRpY2F0aW9uIHN1Y2NlZWRlZCBvciBub3QuIElmIHN1Y2Nlc3NmdWxseSBhdXRoZW50aWNhdGVkXG4gICAqIGVtaXQgYGlkbGVgIHRvIGluZGljYXRlIHRoYXQgYW4gZS1tYWlsIGNhbiBiZSBzZW50IHVzaW5nIHRoaXMgY29ubmVjdGlvblxuICAgKlxuICAgKiBAcGFyYW0ge09iamVjdH0gY29tbWFuZCBQYXJzZWQgY29tbWFuZCBmcm9tIHRoZSBzZXJ2ZXIge3N0YXR1c0NvZGUsIGRhdGEsIGxpbmV9XG4gICAqL1xuICBfYWN0aW9uQVVUSENvbXBsZXRlIChjb21tYW5kKSB7XG4gICAgaWYgKCFjb21tYW5kLnN1Y2Nlc3MpIHtcbiAgICAgIHRoaXMubG9nZ2VyLmRlYnVnKERFQlVHX1RBRywgJ0F1dGhlbnRpY2F0aW9uIGZhaWxlZDogJyArIGNvbW1hbmQuZGF0YSlcbiAgICAgIHRoaXMuX29uRXJyb3IobmV3IEVycm9yKGNvbW1hbmQuZGF0YSkpXG4gICAgICByZXR1cm5cbiAgICB9XG5cbiAgICB0aGlzLmxvZ2dlci5kZWJ1ZyhERUJVR19UQUcsICdBdXRoZW50aWNhdGlvbiBzdWNjZXNzZnVsLicpXG5cbiAgICB0aGlzLl9hdXRoZW50aWNhdGVkQXMgPSB0aGlzLm9wdGlvbnMuYXV0aC51c2VyXG5cbiAgICB0aGlzLl9jdXJyZW50QWN0aW9uID0gdGhpcy5fYWN0aW9uSWRsZVxuICAgIHRoaXMub25pZGxlKCkgLy8gcmVhZHkgdG8gdGFrZSBvcmRlcnNcbiAgfVxuXG4gIC8qKlxuICAgKiBVc2VkIHdoZW4gdGhlIGNvbm5lY3Rpb24gaXMgaWRsZSBhbmQgdGhlIHNlcnZlciBlbWl0cyB0aW1lb3V0XG4gICAqXG4gICAqIEBwYXJhbSB7T2JqZWN0fSBjb21tYW5kIFBhcnNlZCBjb21tYW5kIGZyb20gdGhlIHNlcnZlciB7c3RhdHVzQ29kZSwgZGF0YSwgbGluZX1cbiAgICovXG4gIF9hY3Rpb25JZGxlIChjb21tYW5kKSB7XG4gICAgaWYgKGNvbW1hbmQuc3RhdHVzQ29kZSA+IDMwMCkge1xuICAgICAgdGhpcy5fb25FcnJvcihuZXcgRXJyb3IoY29tbWFuZC5saW5lKSlcbiAgICAgIHJldHVyblxuICAgIH1cblxuICAgIHRoaXMuX29uRXJyb3IobmV3IEVycm9yKGNvbW1hbmQuZGF0YSkpXG4gIH1cblxuICAvKipcbiAgICogUmVzcG9uc2UgdG8gTUFJTCBGUk9NIGNvbW1hbmQuIFByb2NlZWQgdG8gZGVmaW5pbmcgUkNQVCBUTyBsaXN0IGlmIHN1Y2Nlc3NmdWxcbiAgICpcbiAgICogQHBhcmFtIHtPYmplY3R9IGNvbW1hbmQgUGFyc2VkIGNvbW1hbmQgZnJvbSB0aGUgc2VydmVyIHtzdGF0dXNDb2RlLCBkYXRhLCBsaW5lfVxuICAgKi9cbiAgX2FjdGlvbk1BSUwgKGNvbW1hbmQpIHtcbiAgICBpZiAoIWNvbW1hbmQuc3VjY2Vzcykge1xuICAgICAgdGhpcy5sb2dnZXIuZGVidWcoREVCVUdfVEFHLCAnTUFJTCBGUk9NIHVuc3VjY2Vzc2Z1bDogJyArIGNvbW1hbmQuZGF0YSlcbiAgICAgIHRoaXMuX29uRXJyb3IobmV3IEVycm9yKGNvbW1hbmQuZGF0YSkpXG4gICAgICByZXR1cm5cbiAgICB9XG5cbiAgICBpZiAoIXRoaXMuX2VudmVsb3BlLnJjcHRRdWV1ZS5sZW5ndGgpIHtcbiAgICAgIHRoaXMuX29uRXJyb3IobmV3IEVycm9yKCdDYW5cXCd0IHNlbmQgbWFpbCAtIG5vIHJlY2lwaWVudHMgZGVmaW5lZCcpKVxuICAgIH0gZWxzZSB7XG4gICAgICB0aGlzLmxvZ2dlci5kZWJ1ZyhERUJVR19UQUcsICdNQUlMIEZST00gc3VjY2Vzc2Z1bCwgcHJvY2VlZGluZyB3aXRoICcgKyB0aGlzLl9lbnZlbG9wZS5yY3B0UXVldWUubGVuZ3RoICsgJyByZWNpcGllbnRzJylcbiAgICAgIHRoaXMubG9nZ2VyLmRlYnVnKERFQlVHX1RBRywgJ0FkZGluZyByZWNpcGllbnQuLi4nKVxuICAgICAgdGhpcy5fZW52ZWxvcGUuY3VyUmVjaXBpZW50ID0gdGhpcy5fZW52ZWxvcGUucmNwdFF1ZXVlLnNoaWZ0KClcbiAgICAgIHRoaXMuX2N1cnJlbnRBY3Rpb24gPSB0aGlzLl9hY3Rpb25SQ1BUXG4gICAgICB0aGlzLl9zZW5kQ29tbWFuZCgnUkNQVCBUTzo8JyArIHRoaXMuX2VudmVsb3BlLmN1clJlY2lwaWVudCArICc+JylcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogUmVzcG9uc2UgdG8gYSBSQ1BUIFRPIGNvbW1hbmQuIElmIHRoZSBjb21tYW5kIGlzIHVuc3VjY2Vzc2Z1bCwgdHJ5IHRoZSBuZXh0IG9uZSxcbiAgICogYXMgdGhpcyBtaWdodCBiZSByZWxhdGVkIG9ubHkgdG8gdGhlIGN1cnJlbnQgcmVjaXBpZW50LCBub3QgYSBnbG9iYWwgZXJyb3IsIHNvXG4gICAqIHRoZSBmb2xsb3dpbmcgcmVjaXBpZW50cyBtaWdodCBzdGlsbCBiZSB2YWxpZFxuICAgKlxuICAgKiBAcGFyYW0ge09iamVjdH0gY29tbWFuZCBQYXJzZWQgY29tbWFuZCBmcm9tIHRoZSBzZXJ2ZXIge3N0YXR1c0NvZGUsIGRhdGEsIGxpbmV9XG4gICAqL1xuICBfYWN0aW9uUkNQVCAoY29tbWFuZCkge1xuICAgIGlmICghY29tbWFuZC5zdWNjZXNzKSB7XG4gICAgICB0aGlzLmxvZ2dlci53YXJuKERFQlVHX1RBRywgJ1JDUFQgVE8gZmFpbGVkIGZvcjogJyArIHRoaXMuX2VudmVsb3BlLmN1clJlY2lwaWVudClcbiAgICAgIC8vIHRoaXMgaXMgYSBzb2Z0IGVycm9yXG4gICAgICB0aGlzLl9lbnZlbG9wZS5yY3B0RmFpbGVkLnB1c2godGhpcy5fZW52ZWxvcGUuY3VyUmVjaXBpZW50KVxuICAgIH0gZWxzZSB7XG4gICAgICB0aGlzLl9lbnZlbG9wZS5yZXNwb25zZVF1ZXVlLnB1c2godGhpcy5fZW52ZWxvcGUuY3VyUmVjaXBpZW50KVxuICAgIH1cblxuICAgIGlmICghdGhpcy5fZW52ZWxvcGUucmNwdFF1ZXVlLmxlbmd0aCkge1xuICAgICAgaWYgKHRoaXMuX2VudmVsb3BlLnJjcHRGYWlsZWQubGVuZ3RoIDwgdGhpcy5fZW52ZWxvcGUudG8ubGVuZ3RoKSB7XG4gICAgICAgIHRoaXMuX2N1cnJlbnRBY3Rpb24gPSB0aGlzLl9hY3Rpb25EQVRBXG4gICAgICAgIHRoaXMubG9nZ2VyLmRlYnVnKERFQlVHX1RBRywgJ1JDUFQgVE8gZG9uZSwgcHJvY2VlZGluZyB3aXRoIHBheWxvYWQnKVxuICAgICAgICB0aGlzLl9zZW5kQ29tbWFuZCgnREFUQScpXG4gICAgICB9IGVsc2Uge1xuICAgICAgICB0aGlzLl9vbkVycm9yKG5ldyBFcnJvcignQ2FuXFwndCBzZW5kIG1haWwgLSBhbGwgcmVjaXBpZW50cyB3ZXJlIHJlamVjdGVkJykpXG4gICAgICAgIHRoaXMuX2N1cnJlbnRBY3Rpb24gPSB0aGlzLl9hY3Rpb25JZGxlXG4gICAgICB9XG4gICAgfSBlbHNlIHtcbiAgICAgIHRoaXMubG9nZ2VyLmRlYnVnKERFQlVHX1RBRywgJ0FkZGluZyByZWNpcGllbnQuLi4nKVxuICAgICAgdGhpcy5fZW52ZWxvcGUuY3VyUmVjaXBpZW50ID0gdGhpcy5fZW52ZWxvcGUucmNwdFF1ZXVlLnNoaWZ0KClcbiAgICAgIHRoaXMuX2N1cnJlbnRBY3Rpb24gPSB0aGlzLl9hY3Rpb25SQ1BUXG4gICAgICB0aGlzLl9zZW5kQ29tbWFuZCgnUkNQVCBUTzo8JyArIHRoaXMuX2VudmVsb3BlLmN1clJlY2lwaWVudCArICc+JylcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogUmVzcG9uc2UgdG8gdGhlIFJTRVQgY29tbWFuZC4gSWYgc3VjY2Vzc2Z1bCwgY2xlYXIgdGhlIGN1cnJlbnQgYXV0aGVudGljYXRpb25cbiAgICogaW5mb3JtYXRpb24gYW5kIHJlYXV0aGVudGljYXRlLlxuICAgKlxuICAgKiBAcGFyYW0ge09iamVjdH0gY29tbWFuZCBQYXJzZWQgY29tbWFuZCBmcm9tIHRoZSBzZXJ2ZXIge3N0YXR1c0NvZGUsIGRhdGEsIGxpbmV9XG4gICAqL1xuICBfYWN0aW9uUlNFVCAoY29tbWFuZCkge1xuICAgIGlmICghY29tbWFuZC5zdWNjZXNzKSB7XG4gICAgICB0aGlzLmxvZ2dlci5lcnJvcihERUJVR19UQUcsICdSU0VUIHVuc3VjY2Vzc2Z1bCAnICsgY29tbWFuZC5kYXRhKVxuICAgICAgdGhpcy5fb25FcnJvcihuZXcgRXJyb3IoY29tbWFuZC5kYXRhKSlcbiAgICAgIHJldHVyblxuICAgIH1cblxuICAgIHRoaXMuX2F1dGhlbnRpY2F0ZWRBcyA9IG51bGxcbiAgICB0aGlzLl9hdXRoZW50aWNhdGVVc2VyKClcbiAgfVxuXG4gIC8qKlxuICAgKiBSZXNwb25zZSB0byB0aGUgREFUQSBjb21tYW5kLiBTZXJ2ZXIgaXMgbm93IHdhaXRpbmcgZm9yIGEgbWVzc2FnZSwgc28gZW1pdCBgb25yZWFkeWBcbiAgICpcbiAgICogQHBhcmFtIHtPYmplY3R9IGNvbW1hbmQgUGFyc2VkIGNvbW1hbmQgZnJvbSB0aGUgc2VydmVyIHtzdGF0dXNDb2RlLCBkYXRhLCBsaW5lfVxuICAgKi9cbiAgX2FjdGlvbkRBVEEgKGNvbW1hbmQpIHtcbiAgICAvLyByZXNwb25zZSBzaG91bGQgYmUgMzU0IGJ1dCBhY2NvcmRpbmcgdG8gdGhpcyBpc3N1ZSBodHRwczovL2dpdGh1Yi5jb20vZWxlaXRoL2VtYWlsanMvaXNzdWVzLzI0XG4gICAgLy8gc29tZSBzZXJ2ZXJzIG1pZ2h0IHVzZSAyNTAgaW5zdGVhZFxuICAgIGlmIChbMjUwLCAzNTRdLmluZGV4T2YoY29tbWFuZC5zdGF0dXNDb2RlKSA8IDApIHtcbiAgICAgIHRoaXMubG9nZ2VyLmVycm9yKERFQlVHX1RBRywgJ0RBVEEgdW5zdWNjZXNzZnVsICcgKyBjb21tYW5kLmRhdGEpXG4gICAgICB0aGlzLl9vbkVycm9yKG5ldyBFcnJvcihjb21tYW5kLmRhdGEpKVxuICAgICAgcmV0dXJuXG4gICAgfVxuXG4gICAgdGhpcy5fZGF0YU1vZGUgPSB0cnVlXG4gICAgdGhpcy5fY3VycmVudEFjdGlvbiA9IHRoaXMuX2FjdGlvbklkbGVcbiAgICB0aGlzLm9ucmVhZHkodGhpcy5fZW52ZWxvcGUucmNwdEZhaWxlZClcbiAgfVxuXG4gIC8qKlxuICAgKiBSZXNwb25zZSBmcm9tIHRoZSBzZXJ2ZXIsIG9uY2UgdGhlIG1lc3NhZ2Ugc3RyZWFtIGhhcyBlbmRlZCB3aXRoIDxDUj48TEY+LjxDUj48TEY+XG4gICAqIEVtaXRzIGBvbmRvbmVgLlxuICAgKlxuICAgKiBAcGFyYW0ge09iamVjdH0gY29tbWFuZCBQYXJzZWQgY29tbWFuZCBmcm9tIHRoZSBzZXJ2ZXIge3N0YXR1c0NvZGUsIGRhdGEsIGxpbmV9XG4gICAqL1xuICBfYWN0aW9uU3RyZWFtIChjb21tYW5kKSB7XG4gICAgdmFyIHJjcHRcblxuICAgIGlmICh0aGlzLm9wdGlvbnMubG10cCkge1xuICAgICAgLy8gTE1UUCByZXR1cm5zIGEgcmVzcG9uc2UgY29kZSBmb3IgKmV2ZXJ5KiBzdWNjZXNzZnVsbHkgc2V0IHJlY2lwaWVudFxuICAgICAgLy8gRm9yIGV2ZXJ5IHJlY2lwaWVudCB0aGUgbWVzc2FnZSBtaWdodCBzdWNjZWVkIG9yIGZhaWwgaW5kaXZpZHVhbGx5XG5cbiAgICAgIHJjcHQgPSB0aGlzLl9lbnZlbG9wZS5yZXNwb25zZVF1ZXVlLnNoaWZ0KClcbiAgICAgIGlmICghY29tbWFuZC5zdWNjZXNzKSB7XG4gICAgICAgIHRoaXMubG9nZ2VyLmVycm9yKERFQlVHX1RBRywgJ0xvY2FsIGRlbGl2ZXJ5IHRvICcgKyByY3B0ICsgJyBmYWlsZWQuJylcbiAgICAgICAgdGhpcy5fZW52ZWxvcGUucmNwdEZhaWxlZC5wdXNoKHJjcHQpXG4gICAgICB9IGVsc2Uge1xuICAgICAgICB0aGlzLmxvZ2dlci5lcnJvcihERUJVR19UQUcsICdMb2NhbCBkZWxpdmVyeSB0byAnICsgcmNwdCArICcgc3VjY2VlZGVkLicpXG4gICAgICB9XG5cbiAgICAgIGlmICh0aGlzLl9lbnZlbG9wZS5yZXNwb25zZVF1ZXVlLmxlbmd0aCkge1xuICAgICAgICB0aGlzLl9jdXJyZW50QWN0aW9uID0gdGhpcy5fYWN0aW9uU3RyZWFtXG4gICAgICAgIHJldHVyblxuICAgICAgfVxuXG4gICAgICB0aGlzLl9jdXJyZW50QWN0aW9uID0gdGhpcy5fYWN0aW9uSWRsZVxuICAgICAgdGhpcy5vbmRvbmUodHJ1ZSlcbiAgICB9IGVsc2Uge1xuICAgICAgLy8gRm9yIFNNVFAgdGhlIG1lc3NhZ2UgZWl0aGVyIGZhaWxzIG9yIHN1Y2NlZWRzLCB0aGVyZSBpcyBubyBpbmZvcm1hdGlvblxuICAgICAgLy8gYWJvdXQgaW5kaXZpZHVhbCByZWNpcGllbnRzXG5cbiAgICAgIGlmICghY29tbWFuZC5zdWNjZXNzKSB7XG4gICAgICAgIHRoaXMubG9nZ2VyLmVycm9yKERFQlVHX1RBRywgJ01lc3NhZ2Ugc2VuZGluZyBmYWlsZWQuJylcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHRoaXMubG9nZ2VyLmRlYnVnKERFQlVHX1RBRywgJ01lc3NhZ2Ugc2VudCBzdWNjZXNzZnVsbHkuJylcbiAgICAgIH1cblxuICAgICAgdGhpcy5fY3VycmVudEFjdGlvbiA9IHRoaXMuX2FjdGlvbklkbGVcbiAgICAgIHRoaXMub25kb25lKCEhY29tbWFuZC5zdWNjZXNzKVxuICAgIH1cblxuICAgIC8vIElmIHRoZSBjbGllbnQgd2FudGVkIHRvIGRvIHNvbWV0aGluZyBlbHNlIChlZy4gdG8gcXVpdCksIGRvIG5vdCBmb3JjZSBpZGxlXG4gICAgaWYgKHRoaXMuX2N1cnJlbnRBY3Rpb24gPT09IHRoaXMuX2FjdGlvbklkbGUpIHtcbiAgICAgIC8vIFdhaXRpbmcgZm9yIG5ldyBjb25uZWN0aW9uc1xuICAgICAgdGhpcy5sb2dnZXIuZGVidWcoREVCVUdfVEFHLCAnSWRsaW5nIHdoaWxlIHdhaXRpbmcgZm9yIG5ldyBjb25uZWN0aW9ucy4uLicpXG4gICAgICB0aGlzLm9uaWRsZSgpXG4gICAgfVxuICB9XG5cbiAgLyoqXG4gICAqIEJ1aWxkcyBhIGxvZ2luIHRva2VuIGZvciBYT0FVVEgyIGF1dGhlbnRpY2F0aW9uIGNvbW1hbmRcbiAgICpcbiAgICogQHBhcmFtIHtTdHJpbmd9IHVzZXIgRS1tYWlsIGFkZHJlc3Mgb2YgdGhlIHVzZXJcbiAgICogQHBhcmFtIHtTdHJpbmd9IHRva2VuIFZhbGlkIGFjY2VzcyB0b2tlbiBmb3IgdGhlIHVzZXJcbiAgICogQHJldHVybiB7U3RyaW5nfSBCYXNlNjQgZm9ybWF0dGVkIGxvZ2luIHRva2VuXG4gICAqL1xuICBfYnVpbGRYT0F1dGgyVG9rZW4gKHVzZXIsIHRva2VuKSB7XG4gICAgdmFyIGF1dGhEYXRhID0gW1xuICAgICAgJ3VzZXI9JyArICh1c2VyIHx8ICcnKSxcbiAgICAgICdhdXRoPUJlYXJlciAnICsgdG9rZW4sXG4gICAgICAnJyxcbiAgICAgICcnXG4gICAgXVxuICAgIC8vIGJhc2U2NChcInVzZXI9e1VzZXJ9XFx4MDBhdXRoPUJlYXJlciB7VG9rZW59XFx4MDBcXHgwMFwiKVxuICAgIHJldHVybiBlbmNvZGUoYXV0aERhdGEuam9pbignXFx4MDEnKSlcbiAgfVxuXG4gIGNyZWF0ZUxvZ2dlciAoY3JlYXRvciA9IGNyZWF0ZURlZmF1bHRMb2dnZXIpIHtcbiAgICBjb25zdCBsb2dnZXIgPSBjcmVhdG9yKCh0aGlzLm9wdGlvbnMuYXV0aCB8fCB7fSkudXNlciB8fCAnJywgdGhpcy5ob3N0KVxuICAgIHRoaXMubG9nTGV2ZWwgPSB0aGlzLkxPR19MRVZFTF9BTExcbiAgICB0aGlzLmxvZ2dlciA9IHtcbiAgICAgIGRlYnVnOiAoLi4ubXNncykgPT4geyBpZiAoTE9HX0xFVkVMX0RFQlVHID49IHRoaXMubG9nTGV2ZWwpIHsgbG9nZ2VyLmRlYnVnKG1zZ3MpIH0gfSxcbiAgICAgIGluZm86ICguLi5tc2dzKSA9PiB7IGlmIChMT0dfTEVWRUxfSU5GTyA+PSB0aGlzLmxvZ0xldmVsKSB7IGxvZ2dlci5pbmZvKG1zZ3MpIH0gfSxcbiAgICAgIHdhcm46ICguLi5tc2dzKSA9PiB7IGlmIChMT0dfTEVWRUxfV0FSTiA+PSB0aGlzLmxvZ0xldmVsKSB7IGxvZ2dlci53YXJuKG1zZ3MpIH0gfSxcbiAgICAgIGVycm9yOiAoLi4ubXNncykgPT4geyBpZiAoTE9HX0xFVkVMX0VSUk9SID49IHRoaXMubG9nTGV2ZWwpIHsgbG9nZ2VyLmVycm9yKG1zZ3MpIH0gfVxuICAgIH1cbiAgfVxufVxuXG5leHBvcnQgZGVmYXVsdCBTbXRwQ2xpZW50XG4iXX0= -------------------------------------------------------------------------------- /dist/common.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | var LOG_LEVEL_NONE = exports.LOG_LEVEL_NONE = 1000; 7 | var LOG_LEVEL_ERROR = exports.LOG_LEVEL_ERROR = 40; 8 | var LOG_LEVEL_WARN = exports.LOG_LEVEL_WARN = 30; 9 | var LOG_LEVEL_INFO = exports.LOG_LEVEL_INFO = 20; 10 | var LOG_LEVEL_DEBUG = exports.LOG_LEVEL_DEBUG = 10; 11 | var LOG_LEVEL_ALL = exports.LOG_LEVEL_ALL = 0; 12 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9jb21tb24uanMiXSwibmFtZXMiOlsiTE9HX0xFVkVMX05PTkUiLCJMT0dfTEVWRUxfRVJST1IiLCJMT0dfTEVWRUxfV0FSTiIsIkxPR19MRVZFTF9JTkZPIiwiTE9HX0xFVkVMX0RFQlVHIiwiTE9HX0xFVkVMX0FMTCJdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBTyxJQUFNQSwwQ0FBaUIsSUFBdkI7QUFDQSxJQUFNQyw0Q0FBa0IsRUFBeEI7QUFDQSxJQUFNQywwQ0FBaUIsRUFBdkI7QUFDQSxJQUFNQywwQ0FBaUIsRUFBdkI7QUFDQSxJQUFNQyw0Q0FBa0IsRUFBeEI7QUFDQSxJQUFNQyx3Q0FBZ0IsQ0FBdEIiLCJmaWxlIjoiY29tbW9uLmpzIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGNvbnN0IExPR19MRVZFTF9OT05FID0gMTAwMFxuZXhwb3J0IGNvbnN0IExPR19MRVZFTF9FUlJPUiA9IDQwXG5leHBvcnQgY29uc3QgTE9HX0xFVkVMX1dBUk4gPSAzMFxuZXhwb3J0IGNvbnN0IExPR19MRVZFTF9JTkZPID0gMjBcbmV4cG9ydCBjb25zdCBMT0dfTEVWRUxfREVCVUcgPSAxMFxuZXhwb3J0IGNvbnN0IExPR19MRVZFTF9BTEwgPSAwXG4iXX0= -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.LOG_LEVEL_ALL = exports.LOG_LEVEL_DEBUG = exports.LOG_LEVEL_INFO = exports.LOG_LEVEL_WARN = exports.LOG_LEVEL_ERROR = exports.LOG_LEVEL_NONE = undefined; 7 | 8 | var _common = require('./common'); 9 | 10 | Object.defineProperty(exports, 'LOG_LEVEL_NONE', { 11 | enumerable: true, 12 | get: function get() { 13 | return _common.LOG_LEVEL_NONE; 14 | } 15 | }); 16 | Object.defineProperty(exports, 'LOG_LEVEL_ERROR', { 17 | enumerable: true, 18 | get: function get() { 19 | return _common.LOG_LEVEL_ERROR; 20 | } 21 | }); 22 | Object.defineProperty(exports, 'LOG_LEVEL_WARN', { 23 | enumerable: true, 24 | get: function get() { 25 | return _common.LOG_LEVEL_WARN; 26 | } 27 | }); 28 | Object.defineProperty(exports, 'LOG_LEVEL_INFO', { 29 | enumerable: true, 30 | get: function get() { 31 | return _common.LOG_LEVEL_INFO; 32 | } 33 | }); 34 | Object.defineProperty(exports, 'LOG_LEVEL_DEBUG', { 35 | enumerable: true, 36 | get: function get() { 37 | return _common.LOG_LEVEL_DEBUG; 38 | } 39 | }); 40 | Object.defineProperty(exports, 'LOG_LEVEL_ALL', { 41 | enumerable: true, 42 | get: function get() { 43 | return _common.LOG_LEVEL_ALL; 44 | } 45 | }); 46 | 47 | var _client = require('./client'); 48 | 49 | var _client2 = _interopRequireDefault(_client); 50 | 51 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 52 | 53 | exports.default = _client2.default; 54 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9pbmRleC5qcyJdLCJuYW1lcyI6WyJMT0dfTEVWRUxfTk9ORSIsIkxPR19MRVZFTF9FUlJPUiIsIkxPR19MRVZFTF9XQVJOIiwiTE9HX0xFVkVMX0lORk8iLCJMT0dfTEVWRUxfREVCVUciLCJMT0dfTEVWRUxfQUxMIiwiU210cENsaWVudCJdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7O21CQUdFQSxjOzs7Ozs7bUJBQ0FDLGU7Ozs7OzttQkFDQUMsYzs7Ozs7O21CQUNBQyxjOzs7Ozs7bUJBQ0FDLGU7Ozs7OzttQkFDQUMsYTs7OztBQVJGOzs7Ozs7a0JBV2VDLGdCIiwiZmlsZSI6ImluZGV4LmpzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IFNtdHBDbGllbnQgZnJvbSAnLi9jbGllbnQnXG5cbmV4cG9ydCB7XG4gIExPR19MRVZFTF9OT05FLFxuICBMT0dfTEVWRUxfRVJST1IsXG4gIExPR19MRVZFTF9XQVJOLFxuICBMT0dfTEVWRUxfSU5GTyxcbiAgTE9HX0xFVkVMX0RFQlVHLFxuICBMT0dfTEVWRUxfQUxMXG59IGZyb20gJy4vY29tbW9uJ1xuXG5leHBvcnQgZGVmYXVsdCBTbXRwQ2xpZW50XG4iXX0= -------------------------------------------------------------------------------- /dist/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = createDefaultLogger; 7 | 8 | var _common = require('./common'); 9 | 10 | var SESSIONCOUNTER = 0; 11 | 12 | function createDefaultLogger(username, hostname) { 13 | var session = ++SESSIONCOUNTER; 14 | var log = function log(level, messages) { 15 | messages = messages.map(function (msg) { 16 | return typeof msg === 'function' ? msg() : msg; 17 | }); 18 | var date = new Date().toISOString(); 19 | var logMessage = '[' + date + '][' + session + '][' + username + '][' + hostname + '] ' + messages.join(' '); 20 | if (level === _common.LOG_LEVEL_DEBUG) { 21 | console.log('[DEBUG]' + logMessage); 22 | } else if (level === _common.LOG_LEVEL_INFO) { 23 | console.info('[INFO]' + logMessage); 24 | } else if (level === _common.LOG_LEVEL_WARN) { 25 | console.warn('[WARN]' + logMessage); 26 | } else if (level === _common.LOG_LEVEL_ERROR) { 27 | console.error('[ERROR]' + logMessage); 28 | } 29 | }; 30 | 31 | return { 32 | debug: function debug(msgs) { 33 | return log(_common.LOG_LEVEL_DEBUG, msgs); 34 | }, 35 | info: function info(msgs) { 36 | return log(_common.LOG_LEVEL_INFO, msgs); 37 | }, 38 | warn: function warn(msgs) { 39 | return log(_common.LOG_LEVEL_WARN, msgs); 40 | }, 41 | error: function error(msgs) { 42 | return log(_common.LOG_LEVEL_ERROR, msgs); 43 | } 44 | }; 45 | } 46 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9sb2dnZXIuanMiXSwibmFtZXMiOlsiY3JlYXRlRGVmYXVsdExvZ2dlciIsIlNFU1NJT05DT1VOVEVSIiwidXNlcm5hbWUiLCJob3N0bmFtZSIsInNlc3Npb24iLCJsb2ciLCJsZXZlbCIsIm1lc3NhZ2VzIiwibWFwIiwibXNnIiwiZGF0ZSIsIkRhdGUiLCJ0b0lTT1N0cmluZyIsImxvZ01lc3NhZ2UiLCJqb2luIiwiTE9HX0xFVkVMX0RFQlVHIiwiY29uc29sZSIsIkxPR19MRVZFTF9JTkZPIiwiaW5mbyIsIkxPR19MRVZFTF9XQVJOIiwid2FybiIsIkxPR19MRVZFTF9FUlJPUiIsImVycm9yIiwiZGVidWciLCJtc2dzIl0sIm1hcHBpbmdzIjoiOzs7OztrQkFTd0JBLG1COztBQVR4Qjs7QUFPQSxJQUFJQyxpQkFBaUIsQ0FBckI7O0FBRWUsU0FBU0QsbUJBQVQsQ0FBOEJFLFFBQTlCLEVBQXdDQyxRQUF4QyxFQUFrRDtBQUMvRCxNQUFNQyxVQUFVLEVBQUVILGNBQWxCO0FBQ0EsTUFBSUksTUFBTSxTQUFOQSxHQUFNLENBQUNDLEtBQUQsRUFBUUMsUUFBUixFQUFxQjtBQUM3QkEsZUFBV0EsU0FBU0MsR0FBVCxDQUFhO0FBQUEsYUFBTyxPQUFPQyxHQUFQLEtBQWUsVUFBZixHQUE0QkEsS0FBNUIsR0FBb0NBLEdBQTNDO0FBQUEsS0FBYixDQUFYO0FBQ0EsUUFBTUMsT0FBTyxJQUFJQyxJQUFKLEdBQVdDLFdBQVgsRUFBYjtBQUNBLFFBQUlDLG1CQUFpQkgsSUFBakIsVUFBMEJOLE9BQTFCLFVBQXNDRixRQUF0QyxVQUFtREMsUUFBbkQsVUFBZ0VJLFNBQVNPLElBQVQsQ0FBYyxHQUFkLENBQXBFO0FBQ0EsUUFBSVIsVUFBVVMsdUJBQWQsRUFBK0I7QUFDN0JDLGNBQVFYLEdBQVIsQ0FBWSxZQUFZUSxVQUF4QjtBQUNELEtBRkQsTUFFTyxJQUFJUCxVQUFVVyxzQkFBZCxFQUE4QjtBQUNuQ0QsY0FBUUUsSUFBUixDQUFhLFdBQVdMLFVBQXhCO0FBQ0QsS0FGTSxNQUVBLElBQUlQLFVBQVVhLHNCQUFkLEVBQThCO0FBQ25DSCxjQUFRSSxJQUFSLENBQWEsV0FBV1AsVUFBeEI7QUFDRCxLQUZNLE1BRUEsSUFBSVAsVUFBVWUsdUJBQWQsRUFBK0I7QUFDcENMLGNBQVFNLEtBQVIsQ0FBYyxZQUFZVCxVQUExQjtBQUNEO0FBQ0YsR0FiRDs7QUFlQSxTQUFPO0FBQ0xVLFdBQU87QUFBQSxhQUFRbEIsSUFBSVUsdUJBQUosRUFBcUJTLElBQXJCLENBQVI7QUFBQSxLQURGO0FBRUxOLFVBQU07QUFBQSxhQUFRYixJQUFJWSxzQkFBSixFQUFvQk8sSUFBcEIsQ0FBUjtBQUFBLEtBRkQ7QUFHTEosVUFBTTtBQUFBLGFBQVFmLElBQUljLHNCQUFKLEVBQW9CSyxJQUFwQixDQUFSO0FBQUEsS0FIRDtBQUlMRixXQUFPO0FBQUEsYUFBUWpCLElBQUlnQix1QkFBSixFQUFxQkcsSUFBckIsQ0FBUjtBQUFBO0FBSkYsR0FBUDtBQU1EIiwiZmlsZSI6ImxvZ2dlci5qcyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7XG4gIExPR19MRVZFTF9FUlJPUixcbiAgTE9HX0xFVkVMX1dBUk4sXG4gIExPR19MRVZFTF9JTkZPLFxuICBMT0dfTEVWRUxfREVCVUdcbn0gZnJvbSAnLi9jb21tb24nXG5cbmxldCBTRVNTSU9OQ09VTlRFUiA9IDBcblxuZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gY3JlYXRlRGVmYXVsdExvZ2dlciAodXNlcm5hbWUsIGhvc3RuYW1lKSB7XG4gIGNvbnN0IHNlc3Npb24gPSArK1NFU1NJT05DT1VOVEVSXG4gIGxldCBsb2cgPSAobGV2ZWwsIG1lc3NhZ2VzKSA9PiB7XG4gICAgbWVzc2FnZXMgPSBtZXNzYWdlcy5tYXAobXNnID0+IHR5cGVvZiBtc2cgPT09ICdmdW5jdGlvbicgPyBtc2coKSA6IG1zZylcbiAgICBjb25zdCBkYXRlID0gbmV3IERhdGUoKS50b0lTT1N0cmluZygpXG4gICAgbGV0IGxvZ01lc3NhZ2UgPSBgWyR7ZGF0ZX1dWyR7c2Vzc2lvbn1dWyR7dXNlcm5hbWV9XVske2hvc3RuYW1lfV0gJHttZXNzYWdlcy5qb2luKCcgJyl9YFxuICAgIGlmIChsZXZlbCA9PT0gTE9HX0xFVkVMX0RFQlVHKSB7XG4gICAgICBjb25zb2xlLmxvZygnW0RFQlVHXScgKyBsb2dNZXNzYWdlKVxuICAgIH0gZWxzZSBpZiAobGV2ZWwgPT09IExPR19MRVZFTF9JTkZPKSB7XG4gICAgICBjb25zb2xlLmluZm8oJ1tJTkZPXScgKyBsb2dNZXNzYWdlKVxuICAgIH0gZWxzZSBpZiAobGV2ZWwgPT09IExPR19MRVZFTF9XQVJOKSB7XG4gICAgICBjb25zb2xlLndhcm4oJ1tXQVJOXScgKyBsb2dNZXNzYWdlKVxuICAgIH0gZWxzZSBpZiAobGV2ZWwgPT09IExPR19MRVZFTF9FUlJPUikge1xuICAgICAgY29uc29sZS5lcnJvcignW0VSUk9SXScgKyBsb2dNZXNzYWdlKVxuICAgIH1cbiAgfVxuXG4gIHJldHVybiB7XG4gICAgZGVidWc6IG1zZ3MgPT4gbG9nKExPR19MRVZFTF9ERUJVRywgbXNncyksXG4gICAgaW5mbzogbXNncyA9PiBsb2coTE9HX0xFVkVMX0lORk8sIG1zZ3MpLFxuICAgIHdhcm46IG1zZ3MgPT4gbG9nKExPR19MRVZFTF9XQVJOLCBtc2dzKSxcbiAgICBlcnJvcjogbXNncyA9PiBsb2coTE9HX0xFVkVMX0VSUk9SLCBtc2dzKVxuICB9XG59XG4iXX0= -------------------------------------------------------------------------------- /dist/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 10 | 11 | var SmtpResponseParser = function () { 12 | /** 13 | * Generates a parser object for data coming from a SMTP server 14 | */ 15 | function SmtpResponseParser() { 16 | _classCallCheck(this, SmtpResponseParser); 17 | 18 | this.destroyed = false; // If set to true, do not accept any more input 19 | 20 | // Event placeholders 21 | // NB! Errors do not block, the parsing and data emitting continues despite of the errors 22 | this.onerror = function () {}; 23 | this.ondata = function () {}; 24 | this.onend = function () {}; 25 | 26 | this._block = { data: [], lines: [], statusCode: null // If the response is a list, contains previous not yet emitted lines 27 | };this._remainder = ''; // If the complete line is not received yet, contains the beginning of it 28 | } 29 | 30 | /** 31 | * Queue some data from the server for parsing. Only allowed, if 'end' has not been called yet 32 | * 33 | * @param {String} chunk Chunk of data received from the server 34 | */ 35 | 36 | 37 | _createClass(SmtpResponseParser, [{ 38 | key: 'send', 39 | value: function send(chunk) { 40 | if (this.destroyed) { 41 | return this.onerror(new Error('This parser has already been closed, "write" is prohibited')); 42 | } 43 | 44 | // Lines should always end with but you never know, might be only as well 45 | var lines = (this._remainder + (chunk || '')).split(/\r?\n/); 46 | this._remainder = lines.pop(); // not sure if the line has completely arrived yet 47 | 48 | for (var i = 0, len = lines.length; i < len; i++) { 49 | this._processLine(lines[i]); 50 | } 51 | } 52 | 53 | /** 54 | * Indicate that all the data from the server has been received. Can be called only once. 55 | * 56 | * @param {String} [chunk] Chunk of data received from the server 57 | */ 58 | 59 | }, { 60 | key: 'end', 61 | value: function end(chunk) { 62 | if (this.destroyed) { 63 | return this.onerror(new Error('This parser has already been closed, "end" is prohibited')); 64 | } 65 | 66 | if (chunk) { 67 | this.send(chunk); 68 | } 69 | 70 | if (this._remainder) { 71 | this._processLine(this._remainder); 72 | } 73 | 74 | this.destroyed = true; 75 | this.onend(); 76 | } 77 | 78 | // Private API 79 | 80 | /** 81 | * Processes a single and complete line. If it is a continous one (slash after status code), 82 | * queue it to this._block 83 | * 84 | * @param {String} line Complete line of data from the server 85 | */ 86 | 87 | }, { 88 | key: '_processLine', 89 | value: function _processLine(line) { 90 | var match, response; 91 | 92 | // possible input strings for the regex: 93 | // 250-MESSAGE 94 | // 250 MESSAGE 95 | // 250 1.2.3 MESSAGE 96 | 97 | if (!line.trim()) { 98 | // nothing to check, empty line 99 | return; 100 | } 101 | 102 | this._block.lines.push(line); 103 | 104 | if (match = line.match(/^(\d{3})([- ])(?:(\d+\.\d+\.\d+)(?: ))?(.*)/)) { 105 | this._block.data.push(match[4]); 106 | 107 | if (match[2] === '-') { 108 | if (this._block.statusCode && this._block.statusCode !== Number(match[1])) { 109 | this.onerror('Invalid status code ' + match[1] + ' for multi line response (' + this._block.statusCode + ' expected)'); 110 | } else if (!this._block.statusCode) { 111 | this._block.statusCode = Number(match[1]); 112 | } 113 | } else { 114 | response = { 115 | statusCode: Number(match[1]) || 0, 116 | enhancedStatus: match[3] || null, 117 | data: this._block.data.join('\n'), 118 | line: this._block.lines.join('\n') 119 | }; 120 | response.success = response.statusCode >= 200 && response.statusCode < 300; 121 | 122 | this.ondata(response); 123 | this._block = { 124 | data: [], 125 | lines: [], 126 | statusCode: null 127 | }; 128 | this._block.statusCode = null; 129 | } 130 | } else { 131 | this.onerror(new Error('Invalid SMTP response "' + line + '"')); 132 | this.ondata({ 133 | success: false, 134 | statusCode: this._block.statusCode || null, 135 | enhancedStatus: null, 136 | data: [line].join('\n'), 137 | line: this._block.lines.join('\n') 138 | }); 139 | this._block = { 140 | data: [], 141 | lines: [], 142 | statusCode: null 143 | }; 144 | } 145 | } 146 | }]); 147 | 148 | return SmtpResponseParser; 149 | }(); 150 | 151 | exports.default = SmtpResponseParser; 152 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9wYXJzZXIuanMiXSwibmFtZXMiOlsiU210cFJlc3BvbnNlUGFyc2VyIiwiZGVzdHJveWVkIiwib25lcnJvciIsIm9uZGF0YSIsIm9uZW5kIiwiX2Jsb2NrIiwiZGF0YSIsImxpbmVzIiwic3RhdHVzQ29kZSIsIl9yZW1haW5kZXIiLCJjaHVuayIsIkVycm9yIiwic3BsaXQiLCJwb3AiLCJpIiwibGVuIiwibGVuZ3RoIiwiX3Byb2Nlc3NMaW5lIiwic2VuZCIsImxpbmUiLCJtYXRjaCIsInJlc3BvbnNlIiwidHJpbSIsInB1c2giLCJOdW1iZXIiLCJlbmhhbmNlZFN0YXR1cyIsImpvaW4iLCJzdWNjZXNzIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7O0lBQU1BLGtCO0FBQ0o7OztBQUdBLGdDQUFlO0FBQUE7O0FBQ2IsU0FBS0MsU0FBTCxHQUFpQixLQUFqQixDQURhLENBQ1U7O0FBRXZCO0FBQ0E7QUFDQSxTQUFLQyxPQUFMLEdBQWUsWUFBTSxDQUFHLENBQXhCO0FBQ0EsU0FBS0MsTUFBTCxHQUFjLFlBQU0sQ0FBRyxDQUF2QjtBQUNBLFNBQUtDLEtBQUwsR0FBYSxZQUFNLENBQUcsQ0FBdEI7O0FBRUEsU0FBS0MsTUFBTCxHQUFjLEVBQUVDLE1BQU0sRUFBUixFQUFZQyxPQUFPLEVBQW5CLEVBQXVCQyxZQUFZLElBQW5DLENBQTBDO0FBQTFDLEtBQWQsQ0FDQSxLQUFLQyxVQUFMLEdBQWtCLEVBQWxCLENBVmEsQ0FVUTtBQUN0Qjs7QUFFRDs7Ozs7Ozs7O3lCQUtNQyxLLEVBQU87QUFDWCxVQUFJLEtBQUtULFNBQVQsRUFBb0I7QUFDbEIsZUFBTyxLQUFLQyxPQUFMLENBQWEsSUFBSVMsS0FBSixDQUFVLDREQUFWLENBQWIsQ0FBUDtBQUNEOztBQUVEO0FBQ0EsVUFBSUosUUFBUSxDQUFDLEtBQUtFLFVBQUwsSUFBbUJDLFNBQVMsRUFBNUIsQ0FBRCxFQUFrQ0UsS0FBbEMsQ0FBd0MsT0FBeEMsQ0FBWjtBQUNBLFdBQUtILFVBQUwsR0FBa0JG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QUlWbkIsZ0JBQU0sQ0FBQ2EsSUFBRCxFQUFPTyxJQUFQLENBQVksSUFBWixDQUpJO0FBS1ZQLGdCQUFNLEtBQUtkLE1BQUwsQ0FBWUUsS0FBWixDQUFrQm1CLElBQWxCLENBQXVCLElBQXZCO0FBTEksU0FBWjtBQU9BLGFBQUtyQixNQUFMLEdBQWM7QUFDWkMsZ0JBQU0sRUFETTtBQUVaQyxpQkFBTyxFQUZLO0FBR1pDLHNCQUFZO0FBSEEsU0FBZDtBQUtEO0FBQ0Y7Ozs7OztrQkFHWVIsa0IiLCJmaWxlIjoicGFyc2VyLmpzIiwic291cmNlc0NvbnRlbnQiOlsiY2xhc3MgU210cFJlc3BvbnNlUGFyc2VyIHtcbiAgLyoqXG4gICAqIEdlbmVyYXRlcyBhIHBhcnNlciBvYmplY3QgZm9yIGRhdGEgY29taW5nIGZyb20gYSBTTVRQIHNlcnZlclxuICAgKi9cbiAgY29uc3RydWN0b3IgKCkge1xuICAgIHRoaXMuZGVzdHJveWVkID0gZmFsc2UgLy8gSWYgc2V0IHRvIHRydWUsIGRvIG5vdCBhY2NlcHQgYW55IG1vcmUgaW5wdXRcblxuICAgIC8vIEV2ZW50IHBsYWNlaG9sZGVyc1xuICAgIC8vIE5CISBFcnJvcnMgZG8gbm90IGJsb2NrLCB0aGUgcGFyc2luZyBhbmQgZGF0YSBlbWl0dGluZyBjb250aW51ZXMgZGVzcGl0ZSBvZiB0aGUgZXJyb3JzXG4gICAgdGhpcy5vbmVycm9yID0gKCkgPT4geyB9XG4gICAgdGhpcy5vbmRhdGEgPSAoKSA9PiB7IH1cbiAgICB0aGlzLm9uZW5kID0gKCkgPT4geyB9XG5cbiAgICB0aGlzLl9ibG9jayA9IHsgZGF0YTogW10sIGxpbmVzOiBbXSwgc3RhdHVzQ29kZTogbnVsbCB9IC8vIElmIHRoZSByZXNwb25zZSBpcyBhIGxpc3QsIGNvbnRhaW5zIHByZXZpb3VzIG5vdCB5ZXQgZW1pdHRlZCBsaW5lc1xuICAgIHRoaXMuX3JlbWFpbmRlciA9ICcnIC8vIElmIHRoZSBjb21wbGV0ZSBsaW5lIGlzIG5vdCByZWNlaXZlZCB5ZXQsIGNvbnRhaW5zIHRoZSBiZWdpbm5pbmcgb2YgaXRcbiAgfVxuXG4gIC8qKlxuICAgKiBRdWV1ZSBzb21lIGRhdGEgZnJvbSB0aGUgc2VydmVyIGZvciBwYXJzaW5nLiBPbmx5IGFsbG93ZWQsIGlmICdlbmQnIGhhcyBub3QgYmVlbiBjYWxsZWQgeWV0XG4gICAqXG4gICAqIEBwYXJhbSB7U3RyaW5nfSBjaHVuayBDaHVuayBvZiBkYXRhIHJlY2VpdmVkIGZyb20gdGhlIHNlcnZlclxuICAgKi9cbiAgc2VuZCAoY2h1bmspIHtcbiAgICBpZiAodGhpcy5kZXN0cm95ZWQpIHtcbiAgICAgIHJldHVybiB0aGlzLm9uZXJyb3IobmV3IEVycm9yKCdUaGlzIHBhcnNlciBoYXMgYWxyZWFkeSBiZWVuIGNsb3NlZCwgXCJ3cml0ZVwiIGlzIHByb2hpYml0ZWQnKSlcbiAgICB9XG5cbiAgICAvLyBMaW5lcyBzaG91bGQgYWx3YXlzIGVuZCB3aXRoIDxDUj48TEY+IGJ1dCB5b3UgbmV2ZXIga25vdywgbWlnaHQgYmUgb25seSA8TEY+IGFzIHdlbGxcbiAgICB2YXIgbGluZXMgPSAodGhpcy5fcmVtYWluZGVyICsgKGNodW5rIHx8ICcnKSkuc3BsaXQoL1xccj9cXG4vKVxuICAgIHRoaXMuX3JlbWFpbmRlciA9IGxpbmVzLnBvcCgpIC8vIG5vdCBzdXJlIGlmIHRoZSBsaW5lIGhhcyBjb21wbGV0ZWx5IGFycml2ZWQgeWV0XG5cbiAgICBmb3IgKHZhciBpID0gMCwgbGVuID0gbGluZXMubGVuZ3RoOyBpIDwgbGVuOyBpKyspIHtcbiAgICAgIHRoaXMuX3Byb2Nlc3NMaW5lKGxpbmVzW2ldKVxuICAgIH1cbiAgfVxuXG4gIC8qKlxuICAgKiBJbmRpY2F0ZSB0aGF0IGFsbCB0aGUgZGF0YSBmcm9tIHRoZSBzZXJ2ZXIgaGFzIGJlZW4gcmVjZWl2ZWQuIENhbiBiZSBjYWxsZWQgb25seSBvbmNlLlxuICAgKlxuICAgKiBAcGFyYW0ge1N0cmluZ30gW2NodW5rXSBDaHVuayBvZiBkYXRhIHJlY2VpdmVkIGZyb20gdGhlIHNlcnZlclxuICAgKi9cbiAgZW5kIChjaHVuaykge1xuICAgIGlmICh0aGlzLmRlc3Ryb3llZCkge1xuICAgICAgcmV0dXJuIHRoaXMub25lcnJvcihuZXcgRXJyb3IoJ1RoaXMgcGFyc2VyIGhhcyBhbHJlYWR5IGJlZW4gY2xvc2VkLCBcImVuZFwiIGlzIHByb2hpYml0ZWQnKSlcbiAgICB9XG5cbiAgICBpZiAoY2h1bmspIHtcbiAgICAgIHRoaXMuc2VuZChjaHVuaylcbiAgICB9XG5cbiAgICBpZiAodGhpcy5fcmVtYWluZGVyKSB7XG4gICAgICB0aGlzLl9wcm9jZXNzTGluZSh0aGlzLl9yZW1haW5kZXIpXG4gICAgfVxuXG4gICAgdGhpcy5kZXN0cm95ZWQgPSB0cnVlXG4gICAgdGhpcy5vbmVuZCgpXG4gIH1cblxuICAvLyBQcml2YXRlIEFQSVxuXG4gIC8qKlxuICAgKiBQcm9jZXNzZXMgYSBzaW5nbGUgYW5kIGNvbXBsZXRlIGxpbmUuIElmIGl0IGlzIGEgY29udGlub3VzIG9uZSAoc2xhc2ggYWZ0ZXIgc3RhdHVzIGNvZGUpLFxuICAgKiBxdWV1ZSBpdCB0byB0aGlzLl9ibG9ja1xuICAgKlxuICAgKiBAcGFyYW0ge1N0cmluZ30gbGluZSBDb21wbGV0ZSBsaW5lIG9mIGRhdGEgZnJvbSB0aGUgc2VydmVyXG4gICAqL1xuICBfcHJvY2Vzc0xpbmUgKGxpbmUpIHtcbiAgICB2YXIgbWF0Y2gsIHJlc3BvbnNlXG5cbiAgICAvLyBwb3NzaWJsZSBpbnB1dCBzdHJpbmdzIGZvciB0aGUgcmVnZXg6XG4gICAgLy8gMjUwLU1FU1NBR0VcbiAgICAvLyAyNTAgTUVTU0FHRVxuICAgIC8vIDI1MCAxLjIuMyBNRVNTQUdFXG5cbiAgICBpZiAoIWxpbmUudHJpbSgpKSB7XG4gICAgICAvLyBub3RoaW5nIHRvIGNoZWNrLCBlbXB0eSBsaW5lXG4gICAgICByZXR1cm5cbiAgICB9XG5cbiAgICB0aGlzLl9ibG9jay5saW5lcy5wdXNoKGxpbmUpXG5cbiAgICBpZiAoKG1hdGNoID0gbGluZS5tYXRjaCgvXihcXGR7M30pKFstIF0pKD86KFxcZCtcXC5cXGQrXFwuXFxkKykoPzogKSk/KC4qKS8pKSkge1xuICAgICAgdGhpcy5fYmxvY2suZGF0YS5wdXNoKG1hdGNoWzRdKVxuXG4gICAgICBpZiAobWF0Y2hbMl0gPT09ICctJykge1xuICAgICAgICBpZiAodGhpcy5fYmxvY2suc3RhdHVzQ29kZSAmJiB0aGlzLl9ibG9jay5zdGF0dXNDb2RlICE9PSBOdW1iZXIobWF0Y2hbMV0pKSB7XG4gICAgICAgICAgdGhpcy5vbmVycm9yKCdJbnZhbGlkIHN0YXR1cyBjb2RlICcgKyBtYXRjaFsxXSArXG4gICAgICAgICAgICAnIGZvciBtdWx0aSBsaW5lIHJlc3BvbnNlICgnICsgdGhpcy5fYmxvY2suc3RhdHVzQ29kZSArICcgZXhwZWN0ZWQpJylcbiAgICAgICAgfSBlbHNlIGlmICghdGhpcy5fYmxvY2suc3RhdHVzQ29kZSkge1xuICAgICAgICAgIHRoaXMuX2Jsb2NrLnN0YXR1c0NvZGUgPSBOdW1iZXIobWF0Y2hbMV0pXG4gICAgICAgIH1cbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHJlc3BvbnNlID0ge1xuICAgICAgICAgIHN0YXR1c0NvZGU6IE51bWJlcihtYXRjaFsxXSkgfHwgMCxcbiAgICAgICAgICBlbmhhbmNlZFN0YXR1czogbWF0Y2hbM10gfHwgbnVsbCxcbiAgICAgICAgICBkYXRhOiB0aGlzLl9ibG9jay5kYXRhLmpvaW4oJ1xcbicpLFxuICAgICAgICAgIGxpbmU6IHRoaXMuX2Jsb2NrLmxpbmVzLmpvaW4oJ1xcbicpXG4gICAgICAgIH1cbiAgICAgICAgcmVzcG9uc2Uuc3VjY2VzcyA9IHJlc3BvbnNlLnN0YXR1c0NvZGUgPj0gMjAwICYmIHJlc3BvbnNlLnN0YXR1c0NvZGUgPCAzMDBcblxuICAgICAgICB0aGlzLm9uZGF0YShyZXNwb25zZSlcbiAgICAgICAgdGhpcy5fYmxvY2sgPSB7XG4gICAgICAgICAgZGF0YTogW10sXG4gICAgICAgICAgbGluZXM6IFtdLFxuICAgICAgICAgIHN0YXR1c0NvZGU6IG51bGxcbiAgICAgICAgfVxuICAgICAgICB0aGlzLl9ibG9jay5zdGF0dXNDb2RlID0gbnVsbFxuICAgICAgfVxuICAgIH0gZWxzZSB7XG4gICAgICB0aGlzLm9uZXJyb3IobmV3IEVycm9yKCdJbnZhbGlkIFNNVFAgcmVzcG9uc2UgXCInICsgbGluZSArICdcIicpKVxuICAgICAgdGhpcy5vbmRhdGEoe1xuICAgICAgICBzdWNjZXNzOiBmYWxzZSxcbiAgICAgICAgc3RhdHVzQ29kZTogdGhpcy5fYmxvY2suc3RhdHVzQ29kZSB8fCBudWxsLFxuICAgICAgICBlbmhhbmNlZFN0YXR1czogbnVsbCxcbiAgICAgICAgZGF0YTogW2xpbmVdLmpvaW4oJ1xcbicpLFxuICAgICAgICBsaW5lOiB0aGlzLl9ibG9jay5saW5lcy5qb2luKCdcXG4nKVxuICAgICAgfSlcbiAgICAgIHRoaXMuX2Jsb2NrID0ge1xuICAgICAgICBkYXRhOiBbXSxcbiAgICAgICAgbGluZXM6IFtdLFxuICAgICAgICBzdGF0dXNDb2RlOiBudWxsXG4gICAgICB9XG4gICAgfVxuICB9XG59XG5cbmV4cG9ydCBkZWZhdWx0IFNtdHBSZXNwb25zZVBhcnNlclxuIl19 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emailjs-smtp-client", 3 | "version": "2.0.1", 4 | "homepage": "https://github.com/emailjs/emailjs-smtp-client", 5 | "description": "SMTP Client allows you to connect to an SMTP server in JS.", 6 | "author": "Andris Reinman ", 7 | "keywords": [ 8 | "SMTP" 9 | ], 10 | "license": "MIT", 11 | "scripts": { 12 | "build": "./scripts/build.sh", 13 | "lint": "$(npm bin)/standard", 14 | "preversion": "npm run build", 15 | "test": "npm run lint && npm run unit && npm run integration", 16 | "unit": "$(npm bin)/mocha './src/*-unit.js' --reporter spec --require babel-register testutils.js", 17 | "integration": "$(npm bin)/mocha './src/*-integration.js' --reporter spec --require babel-register testutils.js" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git://github.com/emailjs/emailjs-smtp-client.git" 22 | }, 23 | "main": "dist/client", 24 | "dependencies": { 25 | "emailjs-base64": "^1.1.4", 26 | "emailjs-tcp-socket": "^2.0.2", 27 | "text-encoding": "^0.7.0", 28 | "winston": "^3.2.1" 29 | }, 30 | "devDependencies": { 31 | "babel-cli": "^6.26.0", 32 | "babel-preset-env": "^1.7.0", 33 | "babel-register": "^6.26.0", 34 | "chai": "^4.2.0", 35 | "mocha": "^6.1.4", 36 | "pre-commit": "^1.2.2", 37 | "sinon": "^7.3.2", 38 | "smtp-server": "^3.5.0", 39 | "standard": "^12.0.1" 40 | }, 41 | "standard": { 42 | "globals": [ 43 | "describe", 44 | "it", 45 | "before", 46 | "beforeEach", 47 | "afterEach", 48 | "after", 49 | "expect", 50 | "sinon" 51 | ], 52 | "ignore": [ 53 | "dist" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf $PWD/dist 4 | babel src --out-dir dist --ignore '**/*-integration.js','**/*-unit.js' --source-maps inline 5 | git reset 6 | git add $PWD/dist 7 | git commit -m 'Updating dist files' -n 8 | -------------------------------------------------------------------------------- /src/client-integration.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | 3 | import SmtpClient from './client' 4 | import { SMTPServer } from 'smtp-server' 5 | 6 | describe('smtp-client data', function () { 7 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' 8 | 9 | let smtp 10 | let port = 10001 11 | let server 12 | 13 | before(function (done) { 14 | server = new SMTPServer({ 15 | port: port, 16 | authOptional: true 17 | }) 18 | server.listen(port, done) 19 | }) 20 | 21 | after(function (done) { 22 | server.close(done) 23 | }) 24 | 25 | beforeEach(function (done) { 26 | smtp = new SmtpClient('127.0.0.1', port, { 27 | useSecureTransport: false 28 | }) 29 | expect(smtp).to.exist 30 | 31 | smtp.connect() 32 | smtp.onidle = function () { 33 | done() 34 | } 35 | }) 36 | 37 | it('should fail with invalid MAIL FROM', function (done) { 38 | smtp.onerror = function (err) { 39 | expect(err.message).to.include('Bad sender address syntax') 40 | smtp.onclose = done 41 | } 42 | 43 | smtp.useEnvelope({ 44 | from: 'invalid', 45 | to: ['receiver@localhost'] 46 | }) 47 | }) 48 | 49 | it('should fail with empty recipients', function (done) { 50 | smtp.onerror = function (err) { 51 | expect(err.message).to.include('Can\'t send mail - no recipients defined') 52 | smtp.onclose = done 53 | } 54 | 55 | smtp.useEnvelope({ 56 | from: 'sender@example.com', 57 | to: [] 58 | }) 59 | }) 60 | 61 | it('should fail with invalid recipients', function (done) { 62 | smtp.onerror = function (err) { 63 | expect(err.message).to.include('Can\'t send mail - all recipients were rejected') 64 | smtp.onclose = done 65 | } 66 | 67 | smtp.useEnvelope({ 68 | from: 'sender@example.com', 69 | to: ['invalid'] 70 | }) 71 | }) 72 | 73 | it('should pass RCPT TO', function (done) { 74 | smtp.onready = function (failed) { 75 | expect(failed).to.deep.equal([]) 76 | smtp.onclose = done 77 | smtp.close() 78 | } 79 | 80 | smtp.useEnvelope({ 81 | from: 'sender@example.com', 82 | to: ['receiver@example.com'] 83 | }) 84 | }) 85 | 86 | it('should pass RCPT TO with some failures', function (done) { 87 | smtp.onready = function (failed) { 88 | expect(failed).to.deep.equal(['invalid']) 89 | smtp.onclose = done 90 | smtp.close() 91 | } 92 | 93 | smtp.useEnvelope({ 94 | from: 'sender@example.com', 95 | to: ['invalid', 'receiver@example.com'] 96 | }) 97 | }) 98 | 99 | it('should succeed with DATA', function (done) { 100 | smtp.onidle = function () { 101 | smtp.onclose = done 102 | smtp.quit() 103 | } 104 | 105 | smtp.onready = function (failedRecipients) { 106 | expect(failedRecipients).to.be.empty 107 | 108 | smtp.send('Subject: test\r\n\r\nMessage body') 109 | smtp.end() 110 | } 111 | 112 | smtp.ondone = function (success) { 113 | expect(success).to.be.true 114 | } 115 | 116 | smtp.useEnvelope({ 117 | from: 'sender@localhost', 118 | to: ['receiver@localhost'] 119 | }) 120 | }) 121 | 122 | it('should not idle', function (done) { 123 | smtp.onidle = function () { 124 | // should not run 125 | expect(true).to.be.false 126 | } 127 | 128 | smtp.onready = function (failedRecipients) { 129 | expect(failedRecipients).to.be.empty 130 | 131 | smtp.send('Subject: test\r\n\r\nMessage body') 132 | smtp.end() 133 | } 134 | 135 | smtp.ondone = function (success) { 136 | expect(success).to.be.true 137 | smtp.onclose = done 138 | smtp.quit() 139 | } 140 | 141 | smtp.useEnvelope({ 142 | from: 'sender@localhost', 143 | to: ['receiver@localhost'] 144 | }) 145 | }) 146 | 147 | it('should timeout', function (done) { 148 | let errored = false 149 | 150 | smtp.onerror = function () { 151 | errored = true 152 | } 153 | 154 | smtp.onclose = function () { 155 | expect(errored).to.be.true 156 | done() 157 | } 158 | 159 | smtp.onready = function (failedRecipients) { 160 | expect(failedRecipients).to.be.empty 161 | 162 | // remove the ondata event to simulate 100% packet loss and make the socket time out after 10ms 163 | smtp.timeoutSocketLowerBound = 10 164 | smtp.timeoutSocketMultiplier = 0 165 | smtp.socket.ondata = function () { } 166 | 167 | smtp.send('Subject: test\r\n\r\nMessage body') // trigger write 168 | } 169 | 170 | smtp.onidle = smtp.ondone = function () { 171 | // should not happen 172 | expect(true).to.be.false 173 | } 174 | 175 | smtp.useEnvelope({ 176 | from: 'sender@localhost', 177 | to: ['receiver@localhost'] 178 | }) 179 | }) 180 | }) 181 | 182 | describe('smtp-client authentication', function () { 183 | let port = 10001 184 | let server 185 | 186 | before(function (done) { 187 | server = new SMTPServer({ 188 | port: port, 189 | closeTimeout: 10, 190 | allowInsecureAuth: true, 191 | authMethods: ['PLAIN', 'LOGIN', 'XOAUTH2'], 192 | onAuth (auth, session, callback) { 193 | if (auth.method === 'PLAIN' && auth.username === 'abc' && auth.password === 'def') { 194 | callback(null, { user: 123 }) 195 | } else if (auth.method === 'LOGIN' && auth.username === 'abc' && auth.password === 'def') { 196 | callback(null, { user: 123 }) 197 | } else if (auth.method === 'XOAUTH2' && auth.username === 'abc' && auth.accessToken === 'def') { 198 | callback(null, { 199 | data: { 200 | status: '401', 201 | schemes: 'bearer mac', 202 | scope: 'my_smtp_access_scope_name' 203 | } 204 | }) 205 | } 206 | callback(new Error('wrong user')) 207 | } 208 | }) 209 | server.listen(port, done) 210 | }) 211 | 212 | after(function (done) { 213 | server.close(done) 214 | }) 215 | 216 | it('should authenticate with default method', function (done) { 217 | let smtp = new SmtpClient('127.0.0.1', port, { 218 | useSecureTransport: false, 219 | auth: { 220 | user: 'abc', 221 | pass: 'def' 222 | } 223 | }) 224 | expect(smtp).to.exist 225 | 226 | smtp.connect() 227 | smtp.onidle = function () { 228 | smtp.onclose = done 229 | setTimeout(() => { smtp.quit() }, 123) 230 | } 231 | }) 232 | 233 | it('should authenticate with AUTH LOGIN', function (done) { 234 | let smtp = new SmtpClient('127.0.0.1', port, { 235 | useSecureTransport: false, 236 | auth: { 237 | user: 'abc', 238 | pass: 'def' 239 | }, 240 | authMethod: 'LOGIN' 241 | }) 242 | expect(smtp).to.exist 243 | 244 | smtp.connect() 245 | smtp.onidle = function () { 246 | smtp.onclose = done 247 | setTimeout(() => { smtp.quit() }, 123) 248 | } 249 | }) 250 | 251 | it('should fail with invalid credentials', function (done) { 252 | let smtp = new SmtpClient('127.0.0.1', port, { 253 | useSecureTransport: false, 254 | auth: { 255 | user: 'abcd', 256 | pass: 'defe' 257 | }, 258 | authMethod: 'LOGIN' 259 | }) 260 | expect(smtp).to.exist 261 | 262 | smtp.connect() 263 | smtp.onerror = function () { 264 | smtp.onclose = done 265 | } 266 | }) 267 | }) 268 | 269 | describe('smtp-client STARTTLS encryption', function () { 270 | let port = 10001 271 | let server 272 | 273 | before(function (done) { 274 | server = new SMTPServer({ 275 | port: port, 276 | authOptional: true 277 | }) 278 | server.listen(port, done) 279 | }) 280 | 281 | after(function (done) { 282 | server.close(done) 283 | }) 284 | 285 | it('should connect insecurely', function (done) { 286 | let smtp = new SmtpClient('127.0.0.1', port, { 287 | useSecureTransport: false, 288 | ignoreTLS: true 289 | }) 290 | expect(smtp).to.exist 291 | 292 | smtp.connect() 293 | smtp.onidle = function () { 294 | expect(smtp._secureMode).to.be.false 295 | smtp.onclose = done 296 | setTimeout(() => { smtp.quit() }, 123) 297 | } 298 | }) 299 | 300 | it('should connect securely', function (done) { 301 | let smtp = new SmtpClient('127.0.0.1', port, { 302 | useSecureTransport: false 303 | }) 304 | expect(smtp).to.exist 305 | 306 | smtp.connect() 307 | smtp.onidle = function () { 308 | expect(smtp._secureMode).to.be.true 309 | smtp.onclose = done 310 | setTimeout(() => { smtp.quit() }, 123) 311 | } 312 | }) 313 | }) 314 | -------------------------------------------------------------------------------- /src/client-unit.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | 3 | import SmtpClient from './client' 4 | 5 | describe('smtpclient unit tests', function () { 6 | let smtp 7 | let host, port, options 8 | let openStub, socketStub 9 | let TCPSocket 10 | 11 | beforeEach(function () { 12 | host = '127.0.0.1' 13 | port = 10000 14 | options = { 15 | useSecureTransport: true, 16 | ca: 'WOW. SUCH CERT. MUCH TLS.' 17 | } 18 | 19 | smtp = new SmtpClient(host, port, options) 20 | expect(smtp).to.exist 21 | 22 | TCPSocket = function () { } 23 | TCPSocket.open = function () { } 24 | TCPSocket.prototype.close = function () { } 25 | TCPSocket.prototype.send = function () { } 26 | TCPSocket.prototype.suspend = function () { } 27 | TCPSocket.prototype.resume = function () { } 28 | TCPSocket.prototype.send = function () { } 29 | TCPSocket.prototype.upgradeToSecure = function () { } 30 | 31 | socketStub = sinon.createStubInstance(TCPSocket) 32 | openStub = sinon.stub(TCPSocket, 'open').withArgs(host, port).returns(socketStub) 33 | 34 | smtp.connect(TCPSocket) 35 | 36 | expect(openStub.callCount).to.equal(1) 37 | expect(socketStub.onopen).to.exist 38 | expect(socketStub.onerror).to.exist 39 | }) 40 | 41 | describe('tcp-socket websocket proxy', function () { 42 | it('should send hostname in onopen', function () { 43 | socketStub.onopen({ 44 | data: { 45 | proxyHostname: 'hostname.io' // hostname of the socket.io proxy in tcp-socket 46 | } 47 | }) 48 | 49 | expect(smtp.options.name).to.equal('hostname.io') 50 | }) 51 | }) 52 | 53 | describe('#connect', function () { 54 | it('should not throw', function () { 55 | let client = new SmtpClient(host, port) 56 | TCPSocket = { 57 | open: function () { 58 | let socket = { 59 | onopen: function () { }, 60 | onerror: function () { } 61 | } 62 | // disallow setting new properties (eg. oncert) 63 | Object.preventExtensions(socket) 64 | return socket 65 | } 66 | } 67 | client.connect(TCPSocket) 68 | }) 69 | }) 70 | 71 | describe('#quit', function () { 72 | it('should send QUIT', function () { 73 | let _sendCommandStub = sinon.stub(smtp, '_sendCommand') 74 | 75 | smtp.quit() 76 | 77 | expect(_sendCommandStub.withArgs('QUIT').callCount).to.equal(1) 78 | }) 79 | }) 80 | 81 | describe('#close', function () { 82 | it('should close socket', function () { 83 | socketStub.readyState = 'open' 84 | smtp.close() 85 | 86 | expect(socketStub.close.callCount).to.equal(1) 87 | }) 88 | 89 | it('should call _destroy', function () { 90 | sinon.stub(smtp, '_destroy') 91 | 92 | socketStub.readyState = '' 93 | smtp.close() 94 | expect(smtp._destroy.callCount).to.equal(1) 95 | }) 96 | }) 97 | 98 | describe('#useEnvelope', function () { 99 | it('should send MAIL FROM', function () { 100 | let envelope = { 101 | from: 'ft', 102 | to: ['tt'] 103 | } 104 | let _sendCommandStub = sinon.stub(smtp, '_sendCommand') 105 | 106 | smtp.useEnvelope(envelope) 107 | 108 | expect(_sendCommandStub.withArgs('MAIL FROM:').callCount).to.equal(1) 109 | expect(smtp._envelope.from).to.deep.equal(envelope.from) 110 | expect(smtp._envelope.to).to.deep.equal(envelope.to) 111 | }) 112 | }) 113 | 114 | describe('#send', function () { 115 | it('should do nothing if not data mode', function () { 116 | smtp._dataMode = false 117 | smtp.send() 118 | 119 | expect(socketStub.send.callCount).to.equal(0) 120 | }) 121 | 122 | it('should send data to socket', function () { 123 | let _sendStringStub = sinon.stub(smtp, '_sendString') 124 | 125 | smtp._dataMode = true 126 | smtp.send('abcde') 127 | 128 | expect(_sendStringStub.withArgs('abcde').callCount).to.equal(1) 129 | }) 130 | }) 131 | 132 | describe('#end', function () { 133 | it('should do nothing if not data mode', function () { 134 | smtp._dataMode = false 135 | smtp.send() 136 | 137 | expect(socketStub.send.callCount).to.equal(0) 138 | }) 139 | 140 | it('should send a dot in a separate line', function () { 141 | smtp._dataMode = true 142 | smtp.end() 143 | 144 | expect(socketStub.send.callCount).to.equal(1) 145 | expect(socketStub.send.args[0][0]).to.deep.equal( 146 | new Uint8Array([13, 10, 46, 13, 10]).buffer) // \r\n.\r\n 147 | }) 148 | }) 149 | 150 | describe('#_parse', function () { 151 | it('should parse and emit a single line response', function () { 152 | sinon.stub(smtp, '_onCommand') 153 | 154 | smtp._parse('250 1.1.1 Ok\r\n') 155 | expect(smtp._onCommand.withArgs({ 156 | statusCode: 250, 157 | data: 'Ok', 158 | success: true 159 | }).callCount).to.equal(1) 160 | }) 161 | 162 | it('should parse and emit a multi line response', function () { 163 | sinon.stub(smtp, '_onCommand') 164 | 165 | smtp._parse('250-Ok 1\r\n') 166 | smtp._parse('250-Ok 2\r\n') 167 | smtp._parse('250 Ok 3\r\n') 168 | 169 | expect(smtp._onCommand.withArgs({ 170 | statusCode: 250, 171 | data: 'Ok 1\nOk 2\nOk 3', 172 | success: true 173 | }).callCount).to.equal(1) 174 | }) 175 | }) 176 | 177 | describe('#_onData', function () { 178 | it('should decode and send chunk to parser', function () { 179 | sinon.stub(smtp, '_parse') 180 | 181 | smtp._onData({ 182 | data: new Uint8Array([97, 98, 99]).buffer // abc 183 | }) 184 | 185 | expect(smtp._parse.withArgs('abc').callCount).to.equal(1) 186 | }) 187 | }) 188 | 189 | describe('#_onDrain', function () { 190 | it('should emit ondrain', function () { 191 | let _ondrainStub = sinon.stub(smtp, 'ondrain') 192 | 193 | smtp._onDrain() 194 | 195 | expect(_ondrainStub.callCount).to.equal(1) 196 | }) 197 | }) 198 | 199 | describe('#_onError', function () { 200 | it('should emit onerror and close connection', function () { 201 | let _onerrorStub = sinon.stub(smtp, 'onerror') 202 | let _closeStub = sinon.stub(smtp, 'close') 203 | let err = new Error('abc') 204 | 205 | smtp._onError({ 206 | data: err 207 | }) 208 | 209 | expect(_onerrorStub.withArgs(err).callCount).to.equal(1) 210 | expect(_closeStub.callCount).to.equal(1) 211 | }) 212 | }) 213 | 214 | describe('#_onClose', function () { 215 | it('should call _destroy', function () { 216 | let _destroyStub = sinon.stub(smtp, '_destroy') 217 | 218 | smtp._onClose() 219 | 220 | expect(_destroyStub.callCount).to.equal(1) 221 | }) 222 | }) 223 | 224 | describe('#_onCommand', function () { 225 | it('should run stored handler', function () { 226 | let _commandStub = sinon.stub() 227 | let cmd = 'abc' 228 | 229 | smtp._currentAction = _commandStub 230 | smtp._onCommand(cmd) 231 | 232 | expect(_commandStub.withArgs(cmd).callCount).to.equal(1) 233 | }) 234 | }) 235 | 236 | describe('#_destroy', function () { 237 | it('should do nothing if already destroyed', function () { 238 | let _oncloseStub = sinon.stub(smtp, 'onclose') 239 | 240 | smtp.destroyed = true 241 | smtp._destroy() 242 | 243 | expect(_oncloseStub.callCount).to.equal(0) 244 | }) 245 | 246 | it('should emit onclose if not destroyed yet', function () { 247 | let _oncloseStub = sinon.stub(smtp, 'onclose') 248 | 249 | smtp.destroyed = false 250 | smtp._destroy() 251 | 252 | expect(_oncloseStub.callCount).to.equal(1) 253 | }) 254 | }) 255 | 256 | describe('#_sendCommand', function () { 257 | it('should convert string to ArrayBuffer and send to socket', function () { 258 | smtp._sendCommand('abc') 259 | 260 | expect(socketStub.send.args[0][0]).to.deep.equal( 261 | new Uint8Array([97, 98, 99, 13, 10]).buffer) // abc\r\n 262 | }) 263 | }) 264 | 265 | describe('_authenticateUser', function () { 266 | it('should emit onidle if no auth info', function () { 267 | let _onidleStub = sinon.stub(smtp, 'onidle') 268 | 269 | smtp.options.auth = false 270 | smtp._authenticateUser() 271 | 272 | expect(_onidleStub.callCount).to.equal(1) 273 | expect(smtp._currentAction).to.equal(smtp._actionIdle) 274 | }) 275 | 276 | it('should use AUTH PLAIN by default', function () { 277 | let _sendCommandStub = sinon.stub(smtp, '_sendCommand') 278 | 279 | smtp.options.auth = { 280 | user: 'abc', 281 | pass: 'def' 282 | } 283 | smtp._supportedAuth = [] 284 | smtp._authenticateUser() 285 | 286 | expect(_sendCommandStub.withArgs('AUTH PLAIN AGFiYwBkZWY=').callCount).to.equal(1) 287 | expect(smtp._currentAction).to.equal(smtp._actionAUTHComplete) 288 | }) 289 | 290 | it('should use AUTH LOGIN if specified', function () { 291 | let _sendCommandStub = sinon.stub(smtp, '_sendCommand') 292 | 293 | smtp.options.auth = { 294 | user: 'abc', 295 | pass: 'def' 296 | } 297 | smtp._supportedAuth = [] 298 | smtp.options.authMethod = 'LOGIN' 299 | smtp._authenticateUser() 300 | 301 | expect(_sendCommandStub.withArgs('AUTH LOGIN').callCount).to.equal(1) 302 | expect(smtp._currentAction).to.equal(smtp._actionAUTH_LOGIN_USER) 303 | }) 304 | 305 | it('should use AUTH XOAUTH2 if specified', function () { 306 | let _sendCommandStub = sinon.stub(smtp, '_sendCommand') 307 | 308 | smtp.options.auth = { 309 | user: 'abc', 310 | xoauth2: 'def' 311 | } 312 | smtp._supportedAuth = ['XOAUTH2'] 313 | smtp._authenticateUser() 314 | 315 | expect(_sendCommandStub.withArgs('AUTH XOAUTH2 dXNlcj1hYmMBYXV0aD1CZWFyZXIgZGVmAQE=').callCount).to.equal(1) 316 | expect(smtp._currentAction).to.equal(smtp._actionAUTH_XOAUTH2) 317 | }) 318 | }) 319 | 320 | describe('#_actionGreeting', function () { 321 | it('should fail if response is not 220', function () { 322 | let _onErrorStub = sinon.stub(smtp, '_onError') 323 | 324 | smtp._actionGreeting({ 325 | statusCode: 500, 326 | data: 'test' 327 | }) 328 | 329 | expect(_onErrorStub.calledOnce).to.be.true 330 | expect(_onErrorStub.args[0][0].message).to.deep.equal('Invalid greeting: test') 331 | }) 332 | 333 | it('should send EHLO on greeting', function () { 334 | let _sendCommandStub = sinon.stub(smtp, '_sendCommand') 335 | 336 | smtp.options.name = 'abc' 337 | smtp._actionGreeting({ 338 | statusCode: 220, 339 | data: 'test' 340 | }) 341 | 342 | expect(_sendCommandStub.withArgs('EHLO abc').callCount).to.equal(1) 343 | expect(smtp._currentAction).to.equal(smtp._actionEHLO) 344 | }) 345 | 346 | it('should send LHLO on greeting', function () { 347 | let _sendCommandStub = sinon.stub(smtp, '_sendCommand') 348 | 349 | smtp.options.name = 'abc' 350 | smtp.options.lmtp = true 351 | smtp._actionGreeting({ 352 | statusCode: 220, 353 | data: 'test' 354 | }) 355 | 356 | expect(_sendCommandStub.withArgs('LHLO abc').callCount).to.equal(1) 357 | expect(smtp._currentAction).to.equal(smtp._actionLHLO) 358 | }) 359 | }) 360 | 361 | describe('#_actionLHLO', function () { 362 | it('should proceed to EHLO', function () { 363 | let _actionEHLOStub = sinon.stub(smtp, '_actionEHLO') 364 | 365 | smtp.options.name = 'abc' 366 | smtp._actionLHLO({ 367 | success: true, 368 | data: 'AUTH PLAIN LOGIN' 369 | }) 370 | 371 | expect(_actionEHLOStub.callCount).to.equal(1) 372 | }) 373 | }) 374 | 375 | describe('#_actionEHLO', function () { 376 | it('should fallback to HELO on error', function () { 377 | let _sendCommandStub = sinon.stub(smtp, '_sendCommand') 378 | 379 | smtp.options.name = 'abc' 380 | smtp._actionEHLO({ 381 | success: false 382 | }) 383 | 384 | expect(_sendCommandStub.withArgs('HELO abc').callCount).to.equal(1) 385 | expect(smtp._currentAction).to.equal(smtp._actionHELO) 386 | }) 387 | 388 | it('should proceed to authentication', function () { 389 | let _authenticateUserStub = sinon.stub(smtp, '_authenticateUser') 390 | 391 | smtp._actionEHLO({ 392 | success: true, 393 | data: 'AUTH PLAIN LOGIN' 394 | }) 395 | 396 | expect(_authenticateUserStub.callCount).to.equal(1) 397 | expect(smtp._supportedAuth).to.deep.equal(['PLAIN', 'LOGIN']) 398 | }) 399 | 400 | it('should proceed to starttls', function () { 401 | let _sendCommandStub = sinon.stub(smtp, '_sendCommand') 402 | 403 | smtp._secureMode = false 404 | smtp._actionEHLO({ 405 | success: true, 406 | data: 'STARTTLS' 407 | }) 408 | 409 | expect(_sendCommandStub.withArgs('STARTTLS').callCount).to.equal(1) 410 | 411 | expect(smtp._currentAction).to.equal(smtp._actionSTARTTLS) 412 | }) 413 | }) 414 | 415 | describe('#_actionHELO', function () { 416 | it('should proceed to authentication', function () { 417 | let _authenticateUserStub = sinon.stub(smtp, '_authenticateUser') 418 | 419 | smtp._actionHELO({ 420 | success: true 421 | }) 422 | 423 | expect(_authenticateUserStub.callCount).to.equal(1) 424 | }) 425 | }) 426 | 427 | describe('#_actionSTARTTLS', function () { 428 | it('should upgrade connection', function () { 429 | let _sendCommandStub = sinon.stub(smtp, '_sendCommand') 430 | 431 | smtp.options.name = 'abc' 432 | smtp._actionSTARTTLS({ 433 | success: true, 434 | data: 'Ready to start TLS' 435 | }) 436 | 437 | expect(smtp.socket.upgradeToSecure.callCount).to.equal(1) 438 | expect(_sendCommandStub.withArgs('EHLO abc').callCount).to.equal(1) 439 | expect(smtp._currentAction).to.equal(smtp._actionEHLO) 440 | }) 441 | }) 442 | 443 | describe('#_actionAUTH_LOGIN_USER', function () { 444 | it('should emit error on invalid input', function () { 445 | let _onErrorStub = sinon.stub(smtp, '_onError') 446 | 447 | smtp._actionAUTH_LOGIN_USER({ 448 | statusCode: 334, // valid status code 449 | data: 'test' // invalid value 450 | }) 451 | 452 | expect(_onErrorStub.callCount).to.equal(1) 453 | expect(_onErrorStub.args[0][0] instanceof Error).to.be.true 454 | }) 455 | 456 | it('should respond to server with base64 encoded username', function () { 457 | let _sendCommandStub = sinon.stub(smtp, '_sendCommand') 458 | 459 | smtp.options.auth = { 460 | user: 'abc', 461 | pass: 'def' 462 | } 463 | smtp._actionAUTH_LOGIN_USER({ 464 | statusCode: 334, 465 | data: 'VXNlcm5hbWU6' 466 | }) 467 | 468 | expect(_sendCommandStub.withArgs('YWJj').callCount).to.equal(1) 469 | expect(smtp._currentAction).to.equal(smtp._actionAUTH_LOGIN_PASS) 470 | }) 471 | }) 472 | 473 | describe('#_actionAUTH_LOGIN_PASS', function () { 474 | it('should emit error on invalid input', function () { 475 | let _onErrorStub = sinon.stub(smtp, '_onError') 476 | 477 | smtp._actionAUTH_LOGIN_PASS({ 478 | statusCode: 334, // valid status code 479 | data: 'test' // invalid value 480 | }) 481 | 482 | expect(_onErrorStub.callCount).to.equal(1) 483 | expect(_onErrorStub.args[0][0] instanceof Error).to.be.true 484 | }) 485 | 486 | it('should respond to server with base64 encoded password', function () { 487 | let _sendCommandStub = sinon.stub(smtp, '_sendCommand') 488 | 489 | smtp.options.auth = { 490 | user: 'abc', 491 | pass: 'def' 492 | } 493 | smtp._actionAUTH_LOGIN_PASS({ 494 | statusCode: 334, 495 | data: 'UGFzc3dvcmQ6' 496 | }) 497 | 498 | expect(_sendCommandStub.withArgs('ZGVm').callCount).to.equal(1) 499 | expect(smtp._currentAction).to.equal(smtp._actionAUTHComplete) 500 | }) 501 | }) 502 | 503 | describe('#_actionAUTH_XOAUTH2', function () { 504 | it('should send empty response on error', function () { 505 | let _sendCommandStub = sinon.stub(smtp, '_sendCommand') 506 | 507 | smtp._actionAUTH_XOAUTH2({ 508 | success: false 509 | }) 510 | 511 | expect(_sendCommandStub.withArgs('').callCount).to.equal(1) 512 | expect(smtp._currentAction).to.equal(smtp._actionAUTHComplete) 513 | }) 514 | 515 | it('should run _actionAUTHComplete on success', function () { 516 | let _actionAUTHCompleteStub = sinon.stub(smtp, '_actionAUTHComplete') 517 | 518 | let cmd = { 519 | success: true 520 | } 521 | smtp._actionAUTH_XOAUTH2(cmd) 522 | 523 | expect(_actionAUTHCompleteStub.withArgs(cmd).callCount).to.equal(1) 524 | }) 525 | }) 526 | 527 | describe('#_actionAUTHComplete', function () { 528 | it('should emit error on invalid auth', function () { 529 | let _onErrorStub = sinon.stub(smtp, '_onError') 530 | 531 | smtp._actionAUTHComplete({ 532 | success: false, 533 | data: 'err' 534 | }) 535 | 536 | expect(_onErrorStub.callCount).to.equal(1) 537 | expect(_onErrorStub.args[0][0] instanceof Error).to.be.true 538 | }) 539 | 540 | it('should emit idle if auth succeeded', function () { 541 | let _onidleStub = sinon.stub(smtp, 'onidle') 542 | 543 | smtp.options.auth = { 544 | user: 'abc', 545 | pass: 'def' 546 | } 547 | smtp._actionAUTHComplete({ 548 | success: true 549 | }) 550 | 551 | expect(_onidleStub.callCount).to.equal(1) 552 | expect(smtp._currentAction).to.equal(smtp._actionIdle) 553 | expect(smtp._authenticatedAs).to.equal('abc') 554 | }) 555 | }) 556 | 557 | describe('#_actionMAIL', function () { 558 | it('should emit error on invalid input', function () { 559 | let _onErrorStub = sinon.stub(smtp, '_onError') 560 | 561 | smtp._actionMAIL({ 562 | success: false, 563 | data: 'err' 564 | }) 565 | 566 | expect(_onErrorStub.calledOnce).to.be.true 567 | expect(_onErrorStub.args[0][0].message).to.equal('err') 568 | }) 569 | 570 | it('should emit error on empty recipient queue', function () { 571 | let _onErrorStub = sinon.stub(smtp, '_onError') 572 | 573 | smtp._envelope = { 574 | rcptQueue: [] 575 | } 576 | smtp._actionMAIL({ 577 | success: true 578 | }) 579 | 580 | expect(_onErrorStub.callCount).to.equal(1) 581 | expect(_onErrorStub.args[0][0] instanceof Error).to.be.true 582 | }) 583 | 584 | it('should send to the next recipient in queue', function () { 585 | let _sendCommandStub = sinon.stub(smtp, '_sendCommand') 586 | 587 | smtp._envelope = { 588 | rcptQueue: ['receiver'] 589 | } 590 | smtp._actionMAIL({ 591 | success: true 592 | }) 593 | 594 | expect(_sendCommandStub.withArgs('RCPT TO:').callCount).to.equal(1) 595 | expect(smtp._currentAction).to.equal(smtp._actionRCPT) 596 | }) 597 | }) 598 | 599 | describe('#_actionRCPT', function () { 600 | it('should send DATA if queue is processed', function () { 601 | let _sendCommandStub = sinon.stub(smtp, '_sendCommand') 602 | 603 | smtp._envelope = { 604 | to: ['abc'], 605 | rcptFailed: [], 606 | rcptQueue: [], 607 | responseQueue: [] 608 | } 609 | smtp._actionRCPT({ 610 | success: true 611 | }) 612 | 613 | expect(_sendCommandStub.withArgs('DATA').callCount).to.equal(1) 614 | expect(smtp._currentAction).to.equal(smtp._actionDATA) 615 | }) 616 | 617 | it('should send rerun RCPT if queue is not empty', function () { 618 | let _sendCommandStub = sinon.stub(smtp, '_sendCommand') 619 | 620 | smtp._envelope = { 621 | rcptQueue: ['receiver'], 622 | responseQueue: [] 623 | } 624 | smtp._actionRCPT({ 625 | success: true 626 | }) 627 | 628 | expect(_sendCommandStub.withArgs('RCPT TO:').callCount).to.equal(1) 629 | expect(smtp._currentAction).to.equal(smtp._actionRCPT) 630 | }) 631 | 632 | it('should emit error if all recipients failed', function () { 633 | let _onErrorStub = sinon.stub(smtp, '_onError') 634 | 635 | smtp._envelope = { 636 | to: ['abc'], 637 | rcptFailed: ['abc'], 638 | rcptQueue: [], 639 | responseQueue: [] 640 | } 641 | smtp._actionRCPT({ 642 | success: true 643 | }) 644 | 645 | expect(_onErrorStub.callCount).to.equal(1) 646 | expect(_onErrorStub.args[0][0] instanceof Error).to.be.true 647 | }) 648 | }) 649 | 650 | describe('#_actionDATA', function () { 651 | it('should emit error on invalid input', function () { 652 | let _onErrorStub = sinon.stub(smtp, '_onError') 653 | 654 | smtp._actionDATA({ 655 | statusCode: 500, 656 | data: 'err' 657 | }) 658 | 659 | expect(_onErrorStub.calledOnce).to.be.true 660 | expect(_onErrorStub.args[0][0].message).to.equal('err') 661 | }) 662 | 663 | it('should emit onready on success', function () { 664 | let _onreadyStub = sinon.stub(smtp, 'onready') 665 | 666 | smtp._envelope = { 667 | to: ['abc'], 668 | rcptFailed: ['abc'], 669 | rcptQueue: [] 670 | } 671 | smtp._actionDATA({ 672 | statusCode: 250 673 | }) 674 | 675 | expect(_onreadyStub.withArgs(['abc']).callCount).to.equal(1) 676 | expect(smtp._currentAction).to.equal(smtp._actionIdle) 677 | expect(smtp._dataMode).to.be.true 678 | }) 679 | }) 680 | 681 | describe('#_actionStream', function () { 682 | it('should emit ondone with argument false', function () { 683 | let _ondoneStub = sinon.stub(smtp, 'ondone') 684 | 685 | smtp._actionStream({ 686 | success: false 687 | }) 688 | 689 | expect(_ondoneStub.withArgs(false).callCount).to.equal(1) 690 | }) 691 | 692 | it('should emit ondone with argument true', function () { 693 | let _ondoneStub = sinon.stub(smtp, 'ondone') 694 | 695 | smtp._actionStream({ 696 | success: true 697 | }) 698 | 699 | expect(_ondoneStub.withArgs(true).callCount).to.equal(1) 700 | }) 701 | 702 | it('should emit onidle if required', function () { 703 | let _onidleStub = sinon.stub(smtp, 'onidle') 704 | 705 | smtp._currentAction = smtp._actionIdle 706 | smtp._actionStream({ 707 | success: true 708 | }) 709 | 710 | expect(_onidleStub.callCount).to.equal(1) 711 | }) 712 | 713 | it('should cancel onidle', function () { 714 | let _onidleStub = sinon.stub(smtp, 'onidle') 715 | 716 | smtp.ondone = function () { 717 | this._currentAction = false 718 | } 719 | 720 | smtp._actionStream({ 721 | success: true 722 | }) 723 | 724 | expect(_onidleStub.callCount).to.equal(0) 725 | }) 726 | 727 | describe('LMTP responses', function () { 728 | it('should receive single responses', function () { 729 | let _ondoneStub = sinon.stub(smtp, 'ondone') 730 | 731 | smtp.options.lmtp = true 732 | smtp._envelope = { 733 | responseQueue: ['abc'], 734 | rcptFailed: [] 735 | } 736 | 737 | smtp._actionStream({ 738 | success: false 739 | }) 740 | 741 | expect(_ondoneStub.withArgs(true).callCount).to.equal(1) 742 | expect(smtp._envelope.rcptFailed).to.deep.equal(['abc']) 743 | }) 744 | 745 | it('should wait for additional responses', function () { 746 | let _ondoneStub = sinon.stub(smtp, 'ondone') 747 | 748 | smtp.options.lmtp = true 749 | smtp._envelope = { 750 | responseQueue: ['abc', 'def', 'ghi'], 751 | rcptFailed: [] 752 | } 753 | 754 | smtp._actionStream({ 755 | success: false 756 | }) 757 | 758 | smtp._actionStream({ 759 | success: true 760 | }) 761 | 762 | smtp._actionStream({ 763 | success: false 764 | }) 765 | 766 | expect(_ondoneStub.withArgs(true).callCount).to.equal(1) 767 | expect(smtp._envelope.rcptFailed).to.deep.equal(['abc', 'ghi']) 768 | }) 769 | }) 770 | }) 771 | 772 | describe('#_buildXOAuth2Token', function () { 773 | it('should return base64 encoded XOAUTH2 token', function () { 774 | expect(smtp._buildXOAuth2Token('user@host', 'abcde')).to.equal('dXNlcj11c2VyQGhvc3QBYXV0aD1CZWFyZXIgYWJjZGUBAQ==') 775 | }) 776 | }) 777 | }) 778 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | import { encode } from 'emailjs-base64' 4 | import TCPSocket from 'emailjs-tcp-socket' 5 | import { TextDecoder, TextEncoder } from 'text-encoding' 6 | 7 | var DEBUG_TAG = 'SMTP Client' 8 | 9 | /** 10 | * Lower Bound for socket timeout to wait since the last data was written to a socket 11 | */ 12 | const TIMEOUT_SOCKET_LOWER_BOUND = 10000 13 | 14 | /** 15 | * Multiplier for socket timeout: 16 | * 17 | * We assume at least a GPRS connection with 115 kb/s = 14,375 kB/s tops, so 10 KB/s to be on 18 | * the safe side. We can timeout after a lower bound of 10s + (n KB / 10 KB/s). A 1 MB message 19 | * upload would be 110 seconds to wait for the timeout. 10 KB/s === 0.1 s/B 20 | */ 21 | const TIMEOUT_SOCKET_MULTIPLIER = 0.1 22 | 23 | class SmtpClient { 24 | /** 25 | * Creates a connection object to a SMTP server and allows to send mail through it. 26 | * Call `connect` method to inititate the actual connection, the constructor only 27 | * defines the properties but does not actually connect. 28 | * 29 | * NB! The parameter order (host, port) differs from node.js "way" (port, host) 30 | * 31 | * @constructor 32 | * 33 | * @param {String} [host="localhost"] Hostname to conenct to 34 | * @param {Number} [port=25] Port number to connect to 35 | * @param {Object} [options] Optional options object 36 | * @param {Boolean} [options.useSecureTransport] Set to true, to use encrypted connection 37 | * @param {String} [options.name] Client hostname for introducing itself to the server 38 | * @param {Object} [options.auth] Authentication options. Depends on the preferred authentication method. Usually {user, pass} 39 | * @param {String} [options.authMethod] Force specific authentication method 40 | * @param {Boolean} [options.disableEscaping] If set to true, do not escape dots on the beginning of the lines 41 | * @param {Boolean} [options.logger] A winston-compatible logger 42 | */ 43 | constructor (host, port, options = {}) { 44 | this.options = options 45 | 46 | this.timeoutSocketLowerBound = TIMEOUT_SOCKET_LOWER_BOUND 47 | this.timeoutSocketMultiplier = TIMEOUT_SOCKET_MULTIPLIER 48 | 49 | this.port = port || (this.options.useSecureTransport ? 465 : 25) 50 | this.host = host || 'localhost' 51 | 52 | /** 53 | * If set to true, start an encrypted connection instead of the plaintext one 54 | * (recommended if applicable). If useSecureTransport is not set but the port used is 465, 55 | * then ecryption is used by default. 56 | */ 57 | this.options.useSecureTransport = 'useSecureTransport' in this.options ? !!this.options.useSecureTransport : this.port === 465 58 | 59 | this.options.auth = this.options.auth || false // Authentication object. If not set, authentication step will be skipped. 60 | this.options.name = this.options.name || 'localhost' // Hostname of the client, this will be used for introducing to the server 61 | this.socket = false // Downstream TCP socket to the SMTP server, created with mozTCPSocket 62 | this.destroyed = false // Indicates if the connection has been closed and can't be used anymore 63 | this.waitDrain = false // Keeps track if the downstream socket is currently full and a drain event should be waited for or not 64 | 65 | // Private properties 66 | 67 | this._authenticatedAs = null // If authenticated successfully, stores the username 68 | this._supportedAuth = [] // A list of authentication mechanisms detected from the EHLO response and which are compatible with this library 69 | this._dataMode = false // If true, accepts data from the upstream to be passed directly to the downstream socket. Used after the DATA command 70 | this._lastDataBytes = '' // Keep track of the last bytes to see how the terminating dot should be placed 71 | this._envelope = null // Envelope object for tracking who is sending mail to whom 72 | this._currentAction = null // Stores the function that should be run after a response has been received from the server 73 | this._secureMode = !!this.options.useSecureTransport // Indicates if the connection is secured or plaintext 74 | this._socketTimeoutTimer = false // Timer waiting to declare the socket dead starting from the last write 75 | this._socketTimeoutStart = false // Start time of sending the first packet in data mode 76 | this._socketTimeoutPeriod = false // Timeout for sending in data mode, gets extended with every send() 77 | 78 | this._parseBlock = { data: [], statusCode: null } 79 | this._parseRemainder = '' // If the complete line is not received yet, contains the beginning of it 80 | 81 | const dummyLogger = ['error', 'warning', 'info', 'debug'].reduce((o, l) => { o[l] = () => {}; return o }, {}) 82 | this.logger = options.logger || dummyLogger 83 | 84 | // Event placeholders 85 | this.onerror = (e) => { } // Will be run when an error occurs. The `onclose` event will fire subsequently. 86 | this.ondrain = () => { } // More data can be buffered in the socket. 87 | this.onclose = () => { } // The connection to the server has been closed 88 | this.onidle = () => { } // The connection is established and idle, you can send mail now 89 | this.onready = (failedRecipients) => { } // Waiting for mail body, lists addresses that were not accepted as recipients 90 | this.ondone = (success) => { } // The mail has been sent. Wait for `onidle` next. Indicates if the message was queued by the server. 91 | } 92 | 93 | /** 94 | * Initiate a connection to the server 95 | */ 96 | connect (SocketContructor = TCPSocket) { 97 | this.socket = SocketContructor.open(this.host, this.port, { 98 | binaryType: 'arraybuffer', 99 | useSecureTransport: this._secureMode, 100 | ca: this.options.ca, 101 | tlsWorkerPath: this.options.tlsWorkerPath, 102 | ws: this.options.ws 103 | }) 104 | 105 | // allows certificate handling for platform w/o native tls support 106 | // oncert is non standard so setting it might throw if the socket object is immutable 107 | try { 108 | this.socket.oncert = this.oncert 109 | } catch (E) { } 110 | this.socket.onerror = this._onError.bind(this) 111 | this.socket.onopen = this._onOpen.bind(this) 112 | } 113 | 114 | /** 115 | * Sends QUIT 116 | */ 117 | quit () { 118 | this.logger.debug(DEBUG_TAG, 'Sending QUIT...') 119 | this._sendCommand('QUIT') 120 | this._currentAction = this.close 121 | } 122 | 123 | /** 124 | * Closes the connection to the server 125 | */ 126 | close () { 127 | this.logger.debug(DEBUG_TAG, 'Closing connection...') 128 | if (this.socket && this.socket.readyState === 'open') { 129 | this.socket.close() 130 | } else { 131 | this._destroy() 132 | } 133 | } 134 | 135 | // Mail related methods 136 | 137 | /** 138 | * Initiates a new message by submitting envelope data, starting with 139 | * `MAIL FROM:` command. Use after `onidle` event 140 | * 141 | * @param {Object} envelope Envelope object in the form of {from:"...", to:["..."]} 142 | */ 143 | useEnvelope (envelope) { 144 | this._envelope = envelope || {} 145 | this._envelope.from = [].concat(this._envelope.from || ('anonymous@' + this.options.name))[0] 146 | this._envelope.to = [].concat(this._envelope.to || []) 147 | 148 | // clone the recipients array for latter manipulation 149 | this._envelope.rcptQueue = [].concat(this._envelope.to) 150 | this._envelope.rcptFailed = [] 151 | this._envelope.responseQueue = [] 152 | 153 | this._currentAction = this._actionMAIL 154 | this.logger.debug(DEBUG_TAG, 'Sending MAIL FROM...') 155 | this._sendCommand('MAIL FROM:<' + (this._envelope.from) + '>') 156 | } 157 | 158 | /** 159 | * Send ASCII data to the server. Works only in data mode (after `onready` event), ignored 160 | * otherwise 161 | * 162 | * @param {String} chunk ASCII string (quoted-printable, base64 etc.) to be sent to the server 163 | * @return {Boolean} If true, it is safe to send more data, if false, you *should* wait for the ondrain event before sending more 164 | */ 165 | send (chunk) { 166 | // works only in data mode 167 | if (!this._dataMode) { 168 | // this line should never be reached but if it does, 169 | // act like everything's normal. 170 | return true 171 | } 172 | 173 | // TODO: if the chunk is an arraybuffer, use a separate function to send the data 174 | return this._sendString(chunk) 175 | } 176 | 177 | /** 178 | * Indicates that a data stream for the socket is ended. Works only in data 179 | * mode (after `onready` event), ignored otherwise. Use it when you are done 180 | * with sending the mail. This method does not close the socket. Once the mail 181 | * has been queued by the server, `ondone` and `onidle` are emitted. 182 | * 183 | * @param {Buffer} [chunk] Chunk of data to be sent to the server 184 | */ 185 | end (chunk) { 186 | // works only in data mode 187 | if (!this._dataMode) { 188 | // this line should never be reached but if it does, 189 | // act like everything's normal. 190 | return true 191 | } 192 | 193 | if (chunk && chunk.length) { 194 | this.send(chunk) 195 | } 196 | 197 | // redirect output from the server to _actionStream 198 | this._currentAction = this._actionStream 199 | 200 | // indicate that the stream has ended by sending a single dot on its own line 201 | // if the client already closed the data with \r\n no need to do it again 202 | if (this._lastDataBytes === '\r\n') { 203 | this.waitDrain = this._send(new Uint8Array([0x2E, 0x0D, 0x0A]).buffer) // .\r\n 204 | } else if (this._lastDataBytes.substr(-1) === '\r') { 205 | this.waitDrain = this._send(new Uint8Array([0x0A, 0x2E, 0x0D, 0x0A]).buffer) // \n.\r\n 206 | } else { 207 | this.waitDrain = this._send(new Uint8Array([0x0D, 0x0A, 0x2E, 0x0D, 0x0A]).buffer) // \r\n.\r\n 208 | } 209 | 210 | // end data mode, reset the variables for extending the timeout in data mode 211 | this._dataMode = false 212 | this._socketTimeoutStart = false 213 | this._socketTimeoutPeriod = false 214 | 215 | return this.waitDrain 216 | } 217 | 218 | // PRIVATE METHODS 219 | 220 | /** 221 | * Queue some data from the server for parsing. 222 | * 223 | * @param {String} chunk Chunk of data received from the server 224 | */ 225 | _parse (chunk) { 226 | // Lines should always end with but you never know, might be only as well 227 | var lines = (this._parseRemainder + (chunk || '')).split(/\r?\n/) 228 | this._parseRemainder = lines.pop() // not sure if the line has completely arrived yet 229 | 230 | for (let i = 0, len = lines.length; i < len; i++) { 231 | if (!lines[i].trim()) { 232 | // nothing to check, empty line 233 | continue 234 | } 235 | 236 | // possible input strings for the regex: 237 | // 250-MULTILINE REPLY 238 | // 250 LAST LINE OF REPLY 239 | // 250 1.2.3 MESSAGE 240 | 241 | const match = lines[i].match(/^(\d{3})([- ])(?:(\d+\.\d+\.\d+)(?: ))?(.*)/) 242 | 243 | if (match) { 244 | this._parseBlock.data.push(match[4]) 245 | 246 | if (match[2] === '-') { 247 | // this is a multiline reply 248 | this._parseBlock.statusCode = this._parseBlock.statusCode || Number(match[1]) 249 | } else { 250 | const statusCode = Number(match[1]) || 0 251 | const response = { 252 | statusCode, 253 | data: this._parseBlock.data.join('\n'), 254 | success: statusCode >= 200 && statusCode < 300 255 | } 256 | 257 | this._onCommand(response) 258 | this._parseBlock = { 259 | data: [], 260 | statusCode: null 261 | } 262 | } 263 | } else { 264 | this._onCommand({ 265 | success: false, 266 | statusCode: this._parseBlock.statusCode || null, 267 | data: [lines[i]].join('\n') 268 | }) 269 | this._parseBlock = { 270 | data: [], 271 | statusCode: null 272 | } 273 | } 274 | } 275 | } 276 | 277 | // EVENT HANDLERS FOR THE SOCKET 278 | 279 | /** 280 | * Connection listener that is run when the connection to the server is opened. 281 | * Sets up different event handlers for the opened socket 282 | * 283 | * @event 284 | * @param {Event} evt Event object. Not used 285 | */ 286 | _onOpen (event) { 287 | if (event && event.data && event.data.proxyHostname) { 288 | this.options.name = event.data.proxyHostname 289 | } 290 | 291 | this.socket.ondata = this._onData.bind(this) 292 | 293 | this.socket.onclose = this._onClose.bind(this) 294 | this.socket.ondrain = this._onDrain.bind(this) 295 | 296 | this._currentAction = this._actionGreeting 297 | } 298 | 299 | /** 300 | * Data listener for chunks of data emitted by the server 301 | * 302 | * @event 303 | * @param {Event} evt Event object. See `evt.data` for the chunk received 304 | */ 305 | _onData (evt) { 306 | clearTimeout(this._socketTimeoutTimer) 307 | var stringPayload = new TextDecoder('UTF-8').decode(new Uint8Array(evt.data)) 308 | this.logger.debug(DEBUG_TAG, 'SERVER: ' + stringPayload) 309 | this._parse(stringPayload) 310 | } 311 | 312 | /** 313 | * More data can be buffered in the socket, `waitDrain` is reset to false 314 | * 315 | * @event 316 | * @param {Event} evt Event object. Not used 317 | */ 318 | _onDrain () { 319 | this.waitDrain = false 320 | this.ondrain() 321 | } 322 | 323 | /** 324 | * Error handler for the socket 325 | * 326 | * @event 327 | * @param {Event} evt Event object. See evt.data for the error 328 | */ 329 | _onError (evt) { 330 | if (evt instanceof Error && evt.message) { 331 | this.logger.error(DEBUG_TAG, evt) 332 | this.onerror(evt) 333 | } else if (evt && evt.data instanceof Error) { 334 | this.logger.error(DEBUG_TAG, evt.data) 335 | this.onerror(evt.data) 336 | } else { 337 | this.logger.error(DEBUG_TAG, new Error((evt && evt.data && evt.data.message) || evt.data || evt || 'Error')) 338 | this.onerror(new Error((evt && evt.data && evt.data.message) || evt.data || evt || 'Error')) 339 | } 340 | 341 | this.close() 342 | } 343 | 344 | /** 345 | * Indicates that the socket has been closed 346 | * 347 | * @event 348 | * @param {Event} evt Event object. Not used 349 | */ 350 | _onClose () { 351 | this.logger.debug(DEBUG_TAG, 'Socket closed.') 352 | this._destroy() 353 | } 354 | 355 | /** 356 | * This is not a socket data handler but the handler for data emitted by the parser, 357 | * so this data is safe to use as it is always complete (server might send partial chunks) 358 | * 359 | * @event 360 | * @param {Object} command Parsed data 361 | */ 362 | _onCommand (command) { 363 | if (typeof this._currentAction === 'function') { 364 | this._currentAction(command) 365 | } 366 | } 367 | 368 | _onTimeout () { 369 | // inform about the timeout and shut down 370 | var error = new Error('Socket timed out!') 371 | this._onError(error) 372 | } 373 | 374 | /** 375 | * Ensures that the connection is closed and such 376 | */ 377 | _destroy () { 378 | clearTimeout(this._socketTimeoutTimer) 379 | 380 | if (!this.destroyed) { 381 | this.destroyed = true 382 | this.onclose() 383 | } 384 | } 385 | 386 | /** 387 | * Sends a string to the socket. 388 | * 389 | * @param {String} chunk ASCII string (quoted-printable, base64 etc.) to be sent to the server 390 | * @return {Boolean} If true, it is safe to send more data, if false, you *should* wait for the ondrain event before sending more 391 | */ 392 | _sendString (chunk) { 393 | // escape dots 394 | if (!this.options.disableEscaping) { 395 | chunk = chunk.replace(/\n\./g, '\n..') 396 | if ((this._lastDataBytes.substr(-1) === '\n' || !this._lastDataBytes) && chunk.charAt(0) === '.') { 397 | chunk = '.' + chunk 398 | } 399 | } 400 | 401 | // Keeping eye on the last bytes sent, to see if there is a sequence 402 | // at the end which is needed to end the data stream 403 | if (chunk.length > 2) { 404 | this._lastDataBytes = chunk.substr(-2) 405 | } else if (chunk.length === 1) { 406 | this._lastDataBytes = this._lastDataBytes.substr(-1) + chunk 407 | } 408 | 409 | this.logger.debug(DEBUG_TAG, 'Sending ' + chunk.length + ' bytes of payload') 410 | 411 | // pass the chunk to the socket 412 | this.waitDrain = this._send(new TextEncoder('UTF-8').encode(chunk).buffer) 413 | return this.waitDrain 414 | } 415 | 416 | /** 417 | * Send a string command to the server, also append \r\n if needed 418 | * 419 | * @param {String} str String to be sent to the server 420 | */ 421 | _sendCommand (str) { 422 | this.waitDrain = this._send(new TextEncoder('UTF-8').encode(str + (str.substr(-2) !== '\r\n' ? '\r\n' : '')).buffer) 423 | } 424 | 425 | _send (buffer) { 426 | this._setTimeout(buffer.byteLength) 427 | return this.socket.send(buffer) 428 | } 429 | 430 | _setTimeout (byteLength) { 431 | var prolongPeriod = Math.floor(byteLength * this.timeoutSocketMultiplier) 432 | var timeout 433 | 434 | if (this._dataMode) { 435 | // we're in data mode, so we count only one timeout that get extended for every send(). 436 | var now = Date.now() 437 | 438 | // the old timeout start time 439 | this._socketTimeoutStart = this._socketTimeoutStart || now 440 | 441 | // the old timeout period, normalized to a minimum of TIMEOUT_SOCKET_LOWER_BOUND 442 | this._socketTimeoutPeriod = (this._socketTimeoutPeriod || this.timeoutSocketLowerBound) + prolongPeriod 443 | 444 | // the new timeout is the delta between the new firing time (= timeout period + timeout start time) and now 445 | timeout = this._socketTimeoutStart + this._socketTimeoutPeriod - now 446 | } else { 447 | // set new timout 448 | timeout = this.timeoutSocketLowerBound + prolongPeriod 449 | } 450 | 451 | clearTimeout(this._socketTimeoutTimer) // clear pending timeouts 452 | this._socketTimeoutTimer = setTimeout(this._onTimeout.bind(this), timeout) // arm the next timeout 453 | } 454 | 455 | /** 456 | * Intitiate authentication sequence if needed 457 | */ 458 | _authenticateUser () { 459 | if (!this.options.auth) { 460 | // no need to authenticate, at least no data given 461 | this._currentAction = this._actionIdle 462 | this.onidle() // ready to take orders 463 | return 464 | } 465 | 466 | var auth 467 | 468 | if (!this.options.authMethod && this.options.auth.xoauth2) { 469 | this.options.authMethod = 'XOAUTH2' 470 | } 471 | 472 | if (this.options.authMethod) { 473 | auth = this.options.authMethod.toUpperCase().trim() 474 | } else { 475 | // use first supported 476 | auth = (this._supportedAuth[0] || 'PLAIN').toUpperCase().trim() 477 | } 478 | 479 | switch (auth) { 480 | case 'LOGIN': 481 | // LOGIN is a 3 step authentication process 482 | // C: AUTH LOGIN 483 | // C: BASE64(USER) 484 | // C: BASE64(PASS) 485 | this.logger.debug(DEBUG_TAG, 'Authentication via AUTH LOGIN') 486 | this._currentAction = this._actionAUTH_LOGIN_USER 487 | this._sendCommand('AUTH LOGIN') 488 | return 489 | case 'PLAIN': 490 | // AUTH PLAIN is a 1 step authentication process 491 | // C: AUTH PLAIN BASE64(\0 USER \0 PASS) 492 | this.logger.debug(DEBUG_TAG, 'Authentication via AUTH PLAIN') 493 | this._currentAction = this._actionAUTHComplete 494 | this._sendCommand( 495 | // convert to BASE64 496 | 'AUTH PLAIN ' + 497 | encode( 498 | // this.options.auth.user+'\u0000'+ 499 | '\u0000' + // skip authorization identity as it causes problems with some servers 500 | this.options.auth.user + '\u0000' + 501 | this.options.auth.pass) 502 | ) 503 | return 504 | case 'XOAUTH2': 505 | // See https://developers.google.com/gmail/xoauth2_protocol#smtp_protocol_exchange 506 | this.logger.debug(DEBUG_TAG, 'Authentication via AUTH XOAUTH2') 507 | this._currentAction = this._actionAUTH_XOAUTH2 508 | this._sendCommand('AUTH XOAUTH2 ' + this._buildXOAuth2Token(this.options.auth.user, this.options.auth.xoauth2)) 509 | return 510 | } 511 | 512 | this._onError(new Error('Unknown authentication method ' + auth)) 513 | } 514 | 515 | // ACTIONS FOR RESPONSES FROM THE SMTP SERVER 516 | 517 | /** 518 | * Initial response from the server, must have a status 220 519 | * 520 | * @param {Object} command Parsed command from the server {statusCode, data} 521 | */ 522 | _actionGreeting (command) { 523 | if (command.statusCode !== 220) { 524 | this._onError(new Error('Invalid greeting: ' + command.data)) 525 | return 526 | } 527 | 528 | if (this.options.lmtp) { 529 | this.logger.debug(DEBUG_TAG, 'Sending LHLO ' + this.options.name) 530 | 531 | this._currentAction = this._actionLHLO 532 | this._sendCommand('LHLO ' + this.options.name) 533 | } else { 534 | this.logger.debug(DEBUG_TAG, 'Sending EHLO ' + this.options.name) 535 | 536 | this._currentAction = this._actionEHLO 537 | this._sendCommand('EHLO ' + this.options.name) 538 | } 539 | } 540 | 541 | /** 542 | * Response to LHLO 543 | * 544 | * @param {Object} command Parsed command from the server {statusCode, data} 545 | */ 546 | _actionLHLO (command) { 547 | if (!command.success) { 548 | this.logger.error(DEBUG_TAG, 'LHLO not successful') 549 | this._onError(new Error(command.data)) 550 | return 551 | } 552 | 553 | // Process as EHLO response 554 | this._actionEHLO(command) 555 | } 556 | 557 | /** 558 | * Response to EHLO. If the response is an error, try HELO instead 559 | * 560 | * @param {Object} command Parsed command from the server {statusCode, data} 561 | */ 562 | _actionEHLO (command) { 563 | var match 564 | 565 | if (!command.success) { 566 | if (!this._secureMode && this.options.requireTLS) { 567 | var errMsg = 'STARTTLS not supported without EHLO' 568 | this.logger.error(DEBUG_TAG, errMsg) 569 | this._onError(new Error(errMsg)) 570 | return 571 | } 572 | 573 | // Try HELO instead 574 | this.logger.warning(DEBUG_TAG, 'EHLO not successful, trying HELO ' + this.options.name) 575 | this._currentAction = this._actionHELO 576 | this._sendCommand('HELO ' + this.options.name) 577 | return 578 | } 579 | 580 | // Detect if the server supports PLAIN auth 581 | if (command.data.match(/AUTH(?:\s+[^\n]*\s+|\s+)PLAIN/i)) { 582 | this.logger.debug(DEBUG_TAG, 'Server supports AUTH PLAIN') 583 | this._supportedAuth.push('PLAIN') 584 | } 585 | 586 | // Detect if the server supports LOGIN auth 587 | if (command.data.match(/AUTH(?:\s+[^\n]*\s+|\s+)LOGIN/i)) { 588 | this.logger.debug(DEBUG_TAG, 'Server supports AUTH LOGIN') 589 | this._supportedAuth.push('LOGIN') 590 | } 591 | 592 | // Detect if the server supports XOAUTH2 auth 593 | if (command.data.match(/AUTH(?:\s+[^\n]*\s+|\s+)XOAUTH2/i)) { 594 | this.logger.debug(DEBUG_TAG, 'Server supports AUTH XOAUTH2') 595 | this._supportedAuth.push('XOAUTH2') 596 | } 597 | 598 | // Detect maximum allowed message size 599 | if ((match = command.data.match(/SIZE (\d+)/i)) && Number(match[1])) { 600 | const maxAllowedSize = Number(match[1]) 601 | this.logger.debug(DEBUG_TAG, 'Maximum allowd message size: ' + maxAllowedSize) 602 | } 603 | 604 | // Detect if the server supports STARTTLS 605 | if (!this._secureMode) { 606 | if ((command.data.match(/STARTTLS\s?$/mi) && !this.options.ignoreTLS) || !!this.options.requireTLS) { 607 | this._currentAction = this._actionSTARTTLS 608 | this.logger.debug(DEBUG_TAG, 'Sending STARTTLS') 609 | this._sendCommand('STARTTLS') 610 | return 611 | } 612 | } 613 | 614 | this._authenticateUser() 615 | } 616 | 617 | /** 618 | * Handles server response for STARTTLS command. If there's an error 619 | * try HELO instead, otherwise initiate TLS upgrade. If the upgrade 620 | * succeedes restart the EHLO 621 | * 622 | * @param {String} str Message from the server 623 | */ 624 | _actionSTARTTLS (command) { 625 | if (!command.success) { 626 | this.logger.error(DEBUG_TAG, 'STARTTLS not successful') 627 | this._onError(new Error(command.data)) 628 | return 629 | } 630 | 631 | this._secureMode = true 632 | this.socket.upgradeToSecure() 633 | 634 | // restart protocol flow 635 | this._currentAction = this._actionEHLO 636 | this._sendCommand('EHLO ' + this.options.name) 637 | } 638 | 639 | /** 640 | * Response to HELO 641 | * 642 | * @param {Object} command Parsed command from the server {statusCode, data} 643 | */ 644 | _actionHELO (command) { 645 | if (!command.success) { 646 | this.logger.error(DEBUG_TAG, 'HELO not successful') 647 | this._onError(new Error(command.data)) 648 | return 649 | } 650 | this._authenticateUser() 651 | } 652 | 653 | /** 654 | * Response to AUTH LOGIN, if successful expects base64 encoded username 655 | * 656 | * @param {Object} command Parsed command from the server {statusCode, data} 657 | */ 658 | _actionAUTH_LOGIN_USER (command) { 659 | if (command.statusCode !== 334 || command.data !== 'VXNlcm5hbWU6') { 660 | this.logger.error(DEBUG_TAG, 'AUTH LOGIN USER not successful: ' + command.data) 661 | this._onError(new Error('Invalid login sequence while waiting for "334 VXNlcm5hbWU6 ": ' + command.data)) 662 | return 663 | } 664 | this.logger.debug(DEBUG_TAG, 'AUTH LOGIN USER successful') 665 | this._currentAction = this._actionAUTH_LOGIN_PASS 666 | this._sendCommand(encode(this.options.auth.user)) 667 | } 668 | 669 | /** 670 | * Response to AUTH LOGIN username, if successful expects base64 encoded password 671 | * 672 | * @param {Object} command Parsed command from the server {statusCode, data} 673 | */ 674 | _actionAUTH_LOGIN_PASS (command) { 675 | if (command.statusCode !== 334 || command.data !== 'UGFzc3dvcmQ6') { 676 | this.logger.error(DEBUG_TAG, 'AUTH LOGIN PASS not successful: ' + command.data) 677 | this._onError(new Error('Invalid login sequence while waiting for "334 UGFzc3dvcmQ6 ": ' + command.data)) 678 | return 679 | } 680 | this.logger.debug(DEBUG_TAG, 'AUTH LOGIN PASS successful') 681 | this._currentAction = this._actionAUTHComplete 682 | this._sendCommand(encode(this.options.auth.pass)) 683 | } 684 | 685 | /** 686 | * Response to AUTH XOAUTH2 token, if error occurs send empty response 687 | * 688 | * @param {Object} command Parsed command from the server {statusCode, data} 689 | */ 690 | _actionAUTH_XOAUTH2 (command) { 691 | if (!command.success) { 692 | this.logger.warning(DEBUG_TAG, 'Error during AUTH XOAUTH2, sending empty response') 693 | this._sendCommand('') 694 | this._currentAction = this._actionAUTHComplete 695 | } else { 696 | this._actionAUTHComplete(command) 697 | } 698 | } 699 | 700 | /** 701 | * Checks if authentication succeeded or not. If successfully authenticated 702 | * emit `idle` to indicate that an e-mail can be sent using this connection 703 | * 704 | * @param {Object} command Parsed command from the server {statusCode, data} 705 | */ 706 | _actionAUTHComplete (command) { 707 | if (!command.success) { 708 | this.logger.debug(DEBUG_TAG, 'Authentication failed: ' + command.data) 709 | this._onError(new Error(command.data)) 710 | return 711 | } 712 | 713 | this.logger.debug(DEBUG_TAG, 'Authentication successful.') 714 | 715 | this._authenticatedAs = this.options.auth.user 716 | 717 | this._currentAction = this._actionIdle 718 | this.onidle() // ready to take orders 719 | } 720 | 721 | /** 722 | * Used when the connection is idle and the server emits timeout 723 | * 724 | * @param {Object} command Parsed command from the server {statusCode, data} 725 | */ 726 | _actionIdle (command) { 727 | if (command.statusCode > 300) { 728 | this._onError(new Error(command.data)) 729 | return 730 | } 731 | 732 | this._onError(new Error(command.data)) 733 | } 734 | 735 | /** 736 | * Response to MAIL FROM command. Proceed to defining RCPT TO list if successful 737 | * 738 | * @param {Object} command Parsed command from the server {statusCode, data} 739 | */ 740 | _actionMAIL (command) { 741 | if (!command.success) { 742 | this.logger.debug(DEBUG_TAG, 'MAIL FROM unsuccessful: ' + command.data) 743 | this._onError(new Error(command.data)) 744 | return 745 | } 746 | 747 | if (!this._envelope.rcptQueue.length) { 748 | this._onError(new Error('Can\'t send mail - no recipients defined')) 749 | } else { 750 | this.logger.debug(DEBUG_TAG, 'MAIL FROM successful, proceeding with ' + this._envelope.rcptQueue.length + ' recipients') 751 | this.logger.debug(DEBUG_TAG, 'Adding recipient...') 752 | this._envelope.curRecipient = this._envelope.rcptQueue.shift() 753 | this._currentAction = this._actionRCPT 754 | this._sendCommand('RCPT TO:<' + this._envelope.curRecipient + '>') 755 | } 756 | } 757 | 758 | /** 759 | * Response to a RCPT TO command. If the command is unsuccessful, try the next one, 760 | * as this might be related only to the current recipient, not a global error, so 761 | * the following recipients might still be valid 762 | * 763 | * @param {Object} command Parsed command from the server {statusCode, data} 764 | */ 765 | _actionRCPT (command) { 766 | if (!command.success) { 767 | this.logger.warning(DEBUG_TAG, 'RCPT TO failed for: ' + this._envelope.curRecipient) 768 | // this is a soft error 769 | this._envelope.rcptFailed.push(this._envelope.curRecipient) 770 | } else { 771 | this._envelope.responseQueue.push(this._envelope.curRecipient) 772 | } 773 | 774 | if (!this._envelope.rcptQueue.length) { 775 | if (this._envelope.rcptFailed.length < this._envelope.to.length) { 776 | this._currentAction = this._actionDATA 777 | this.logger.debug(DEBUG_TAG, 'RCPT TO done, proceeding with payload') 778 | this._sendCommand('DATA') 779 | } else { 780 | this._onError(new Error('Can\'t send mail - all recipients were rejected')) 781 | this._currentAction = this._actionIdle 782 | } 783 | } else { 784 | this.logger.debug(DEBUG_TAG, 'Adding recipient...') 785 | this._envelope.curRecipient = this._envelope.rcptQueue.shift() 786 | this._currentAction = this._actionRCPT 787 | this._sendCommand('RCPT TO:<' + this._envelope.curRecipient + '>') 788 | } 789 | } 790 | 791 | /** 792 | * Response to the DATA command. Server is now waiting for a message, so emit `onready` 793 | * 794 | * @param {Object} command Parsed command from the server {statusCode, data} 795 | */ 796 | _actionDATA (command) { 797 | // response should be 354 but according to this issue https://github.com/eleith/emailjs/issues/24 798 | // some servers might use 250 instead 799 | if ([250, 354].indexOf(command.statusCode) < 0) { 800 | this.logger.error(DEBUG_TAG, 'DATA unsuccessful ' + command.data) 801 | this._onError(new Error(command.data)) 802 | return 803 | } 804 | 805 | this._dataMode = true 806 | this._currentAction = this._actionIdle 807 | this.onready(this._envelope.rcptFailed) 808 | } 809 | 810 | /** 811 | * Response from the server, once the message stream has ended with . 812 | * Emits `ondone`. 813 | * 814 | * @param {Object} command Parsed command from the server {statusCode, data} 815 | */ 816 | _actionStream (command) { 817 | var rcpt 818 | 819 | if (this.options.lmtp) { 820 | // LMTP returns a response code for *every* successfully set recipient 821 | // For every recipient the message might succeed or fail individually 822 | 823 | rcpt = this._envelope.responseQueue.shift() 824 | if (!command.success) { 825 | this.logger.error(DEBUG_TAG, 'Local delivery to ' + rcpt + ' failed.') 826 | this._envelope.rcptFailed.push(rcpt) 827 | } else { 828 | this.logger.error(DEBUG_TAG, 'Local delivery to ' + rcpt + ' succeeded.') 829 | } 830 | 831 | if (this._envelope.responseQueue.length) { 832 | this._currentAction = this._actionStream 833 | return 834 | } 835 | 836 | this._currentAction = this._actionIdle 837 | this.ondone(true) 838 | } else { 839 | // For SMTP the message either fails or succeeds, there is no information 840 | // about individual recipients 841 | 842 | if (!command.success) { 843 | this.logger.error(DEBUG_TAG, 'Message sending failed.') 844 | } else { 845 | this.logger.debug(DEBUG_TAG, 'Message sent successfully.') 846 | } 847 | 848 | this._currentAction = this._actionIdle 849 | this.ondone(!!command.success) 850 | } 851 | 852 | // If the client wanted to do something else (eg. to quit), do not force idle 853 | if (this._currentAction === this._actionIdle) { 854 | // Waiting for new connections 855 | this.logger.debug(DEBUG_TAG, 'Idling while waiting for new connections...') 856 | this.onidle() 857 | } 858 | } 859 | 860 | /** 861 | * Builds a login token for XOAUTH2 authentication command 862 | * 863 | * @param {String} user E-mail address of the user 864 | * @param {String} token Valid access token for the user 865 | * @return {String} Base64 formatted login token 866 | */ 867 | _buildXOAuth2Token (user, token) { 868 | var authData = [ 869 | 'user=' + (user || ''), 870 | 'auth=Bearer ' + token, 871 | '', 872 | '' 873 | ] 874 | // base64("user={User}\x00auth=Bearer {Token}\x00\x00") 875 | return encode(authData.join('\x01')) 876 | } 877 | } 878 | 879 | export default SmtpClient 880 | -------------------------------------------------------------------------------- /testutils.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import sinon from 'sinon' 3 | 4 | global.expect = expect 5 | global.sinon = sinon 6 | --------------------------------------------------------------------------------