diff --git a/node_modules/httpntlm/.jshintrc b/node_modules/httpntlm/.jshintrc
new file mode 100644
index 0000000..f90de4e
--- /dev/null
+++ b/node_modules/httpntlm/.jshintrc
@@ -0,0 +1,4 @@
+{
+  "node": true,
+  "laxbreak": true
+}
diff --git a/node_modules/httpntlm/.npmignore b/node_modules/httpntlm/.npmignore
new file mode 100644
index 0000000..1bbb2d9
--- /dev/null
+++ b/node_modules/httpntlm/.npmignore
@@ -0,0 +1,17 @@
+lib-cov
+*.seed
+*.log
+*.csv
+*.dat
+*.out
+*.pid
+*.gz
+
+pids
+logs
+results
+
+npm-debug.log
+node_modules
+app.js
+test.js
\ No newline at end of file
diff --git a/node_modules/httpntlm/LICENSE b/node_modules/httpntlm/LICENSE
new file mode 100644
index 0000000..c021326
--- /dev/null
+++ b/node_modules/httpntlm/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2013 Sam
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/node_modules/httpntlm/README.md b/node_modules/httpntlm/README.md
new file mode 100644
index 0000000..5cced4a
--- /dev/null
+++ b/node_modules/httpntlm/README.md
@@ -0,0 +1,148 @@
+# httpntlm
+
+__httpntlm__ is a Node.js library to do HTTP NTLM authentication
+
+It's a port from the Python libary [python-ntml](https://code.google.com/p/python-ntlm/)
+
+## Install
+
+You can install __httpntlm__ using the Node Package Manager (npm):
+
+    npm install httpntlm
+
+## How to use
+
+```js
+var httpntlm = require('httpntlm');
+
+httpntlm.get({
+    url: "https://someurl.com",
+    username: 'm$',
+    password: 'stinks',
+    workstation: 'choose.something',
+    domain: ''
+}, function (err, res){
+    if(err) return err;
+
+    console.log(res.headers);
+    console.log(res.body);
+});
+```
+
+It supports __http__ and __https__.
+
+## Options
+
+- `url:`      _{String}_   URL to connect. (Required)
+- `username:` _{String}_   Username. (Required)
+- `password:` _{String}_   Password. (Required)
+- `workstation:` _{String}_ Name of workstation or `''`.
+- `domain:`   _{String}_   Name of domain or `''`.
+
+You can also pass along all other options of [httpreq](https://github.com/SamDecrock/node-httpreq), including custom headers, cookies, body data, ... and use POST, PUT or DELETE instead of GET.
+
+## Advanced
+
+If you want to use the NTLM-functions yourself, you can access the ntlm-library like this (https example):
+
+```js
+var ntlm = require('httpntlm').ntlm;
+var async = require('async');
+var httpreq = require('httpreq');
+var HttpsAgent = require('agentkeepalive').HttpsAgent;
+var keepaliveAgent = new HttpsAgent();
+
+var options = {
+    url: "https://someurl.com",
+    username: 'm$',
+    password: 'stinks',
+    workstation: 'choose.something',
+    domain: ''
+};
+
+async.waterfall([
+    function (callback){
+        var type1msg = ntlm.createType1Message(options);
+
+        httpreq.get(options.url, {
+            headers:{
+                'Connection' : 'keep-alive',
+                'Authorization': type1msg
+            },
+            agent: keepaliveAgent
+        }, callback);
+    },
+
+    function (res, callback){
+        if(!res.headers['www-authenticate'])
+            return callback(new Error('www-authenticate not found on response of second request'));
+
+        var type2msg = ntlm.parseType2Message(res.headers['www-authenticate']);
+        var type3msg = ntlm.createType3Message(type2msg, options);
+
+        setImmediate(function() {
+            httpreq.get(options.url, {
+                headers:{
+                    'Connection' : 'Close',
+                    'Authorization': type3msg
+                },
+                allowRedirects: false,
+                agent: keepaliveAgent
+            }, callback);
+        });
+    }
+], function (err, res) {
+    if(err) return console.log(err);
+
+    console.log(res.headers);
+    console.log(res.body);
+});
+```
+
+## Download binary files
+
+```javascript
+httpntlm.get({
+    url: "https://someurl.com/file.xls",
+    username: 'm$',
+    password: 'stinks',
+    workstation: 'choose.something',
+    domain: '',
+    binary: true
+}, function (err, response) {
+    if(err) return console.log(err);
+    fs.writeFile("file.xls", response.body, function (err) {
+        if(err) return console.log("error writing file");
+        console.log("file.xls saved!");
+    });
+});
+```
+
+## More information
+
+* [python-ntlm](https://code.google.com/p/python-ntlm/)
+* [NTLM Authentication Scheme for HTTP](http://www.innovation.ch/personal/ronald/ntlm.html)
+* [LM hash on Wikipedia](http://en.wikipedia.org/wiki/LM_hash)
+
+
+## License (MIT)
+
+Copyright (c) Sam Decrock <https://github.com/SamDecrock/>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/node_modules/httpntlm/httpntlm.js b/node_modules/httpntlm/httpntlm.js
new file mode 100644
index 0000000..cd71fb3
--- /dev/null
+++ b/node_modules/httpntlm/httpntlm.js
@@ -0,0 +1,104 @@
+'use strict';
+
+var url = require('url');
+var httpreq = require('httpreq');
+var ntlm = require('./ntlm');
+var _ = require('underscore');
+var http = require('http');
+var https = require('https');
+
+exports.method = function(method, options, finalCallback){
+	if(!options.workstation) options.workstation = '';
+	if(!options.domain) options.domain = '';
+
+	// extract non-ntlm-options:
+	var httpreqOptions = _.omit(options, 'url', 'username', 'password', 'workstation', 'domain');
+
+	// is https?
+	var isHttps = false;
+	var reqUrl = url.parse(options.url);
+	if(reqUrl.protocol == 'https:') isHttps = true;
+
+	// set keepaliveAgent (http or https):
+	var keepaliveAgent;
+
+	if(isHttps){
+		keepaliveAgent = new https.Agent({keepAlive: true});
+	}else{
+		keepaliveAgent = new http.Agent({keepAlive: true});
+	}
+
+	// build type1 request:
+
+	function sendType1Message (callback) {
+		var type1msg = ntlm.createType1Message(options);
+
+		var type1options = {
+			headers:{
+				'Connection' : 'keep-alive',
+				'Authorization': type1msg
+			},
+			timeout: options.timeout || 0,
+			agent: keepaliveAgent,
+			allowRedirects: false // don't redirect in httpreq, because http could change to https which means we need to change the keepaliveAgent
+		};
+
+		// pass along timeout and ca:
+		if(httpreqOptions.timeout) type1options.timeout = httpreqOptions.timeout;
+		if(httpreqOptions.ca) type1options.ca = httpreqOptions.ca;
+
+		// send type1 message to server:
+		httpreq.get(options.url, type1options, callback);
+	}
+
+	function sendType3Message (res, callback) {
+		// catch redirect here:
+		if(res.headers.location) {
+			options.url = res.headers.location;
+			return exports[method](options, finalCallback);
+		}
+
+
+		if(!res.headers['www-authenticate'])
+			return callback(new Error('www-authenticate not found on response of second request'));
+
+		// parse type2 message from server:
+		var type2msg = ntlm.parseType2Message(res.headers['www-authenticate']);
+
+		// create type3 message:
+		var type3msg = ntlm.createType3Message(type2msg, options);
+
+		// build type3 request:
+		var type3options = {
+			headers: {
+				'Connection': 'Close',
+				'Authorization': type3msg
+			},
+			allowRedirects: false,
+			agent: keepaliveAgent
+		};
+
+		// pass along other options:
+		type3options.headers = _.extend(type3options.headers, httpreqOptions.headers);
+		type3options = _.extend(type3options, _.omit(httpreqOptions, 'headers'));
+
+		// send type3 message to server:
+		httpreq[method](options.url, type3options, callback);
+	}
+
+
+	sendType1Message(function (err, res) {
+		if(err) return finalCallback(err);
+		setImmediate(function () { // doesn't work without setImmediate()
+			sendType3Message(res, finalCallback);
+		});
+	});
+
+};
+
+['get', 'put', 'patch', 'post', 'delete', 'options'].forEach(function(method){
+	exports[method] = exports.method.bind(exports, method);
+});
+
+exports.ntlm = ntlm; //if you want to use the NTML functions yourself
+
diff --git a/node_modules/httpntlm/node_modules/underscore/LICENSE b/node_modules/httpntlm/node_modules/underscore/LICENSE
new file mode 100644
index 0000000..0d6b873
--- /dev/null
+++ b/node_modules/httpntlm/node_modules/underscore/LICENSE
@@ -0,0 +1,23 @@
+Copyright (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative
+Reporters & Editors
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/node_modules/httpntlm/node_modules/underscore/README.md b/node_modules/httpntlm/node_modules/underscore/README.md
new file mode 100644
index 0000000..c2ba259
--- /dev/null
+++ b/node_modules/httpntlm/node_modules/underscore/README.md
@@ -0,0 +1,22 @@
+                       __
+                      /\ \                                                         __
+     __  __    ___    \_\ \     __   _ __   ____    ___    ___   _ __    __       /\_\    ____
+    /\ \/\ \ /' _ `\  /'_  \  /'__`\/\  __\/ ,__\  / ___\ / __`\/\  __\/'__`\     \/\ \  /',__\
+    \ \ \_\ \/\ \/\ \/\ \ \ \/\  __/\ \ \//\__, `\/\ \__//\ \ \ \ \ \//\  __/  __  \ \ \/\__, `\
+     \ \____/\ \_\ \_\ \___,_\ \____\\ \_\\/\____/\ \____\ \____/\ \_\\ \____\/\_\ _\ \ \/\____/
+      \/___/  \/_/\/_/\/__,_ /\/____/ \/_/ \/___/  \/____/\/___/  \/_/ \/____/\/_//\ \_\ \/___/
+                                                                                  \ \____/
+                                                                                   \/___/
+
+Underscore.js is a utility-belt library for JavaScript that provides
+support for the usual functional suspects (each, map, reduce, filter...)
+without extending any core JavaScript objects.
+
+For Docs, License, Tests, and pre-packed downloads, see:
+http://underscorejs.org
+
+Underscore is an open-sourced component of DocumentCloud:
+https://github.com/documentcloud
+
+Many thanks to our contributors:
+https://github.com/jashkenas/underscore/contributors
diff --git a/node_modules/httpntlm/node_modules/underscore/package.json b/node_modules/httpntlm/node_modules/underscore/package.json
new file mode 100644
index 0000000..8e19743
--- /dev/null
+++ b/node_modules/httpntlm/node_modules/underscore/package.json
@@ -0,0 +1,104 @@
+{
+  "_args": [
+    [
+      {
+        "raw": "underscore@~1.7.0",
+        "scope": null,
+        "escapedName": "underscore",
+        "name": "underscore",
+        "rawSpec": "~1.7.0",
+        "spec": ">=1.7.0 <1.8.0",
+        "type": "range"
+      },
+      "/Users/fzy/project/koa2_Sequelize_project/node_modules/httpntlm"
+    ]
+  ],
+  "_from": "underscore@>=1.7.0 <1.8.0",
+  "_id": "underscore@1.7.0",
+  "_inCache": true,
+  "_location": "/httpntlm/underscore",
+  "_npmUser": {
+    "name": "jashkenas",
+    "email": "jashkenas@gmail.com"
+  },
+  "_npmVersion": "1.4.24",
+  "_phantomChildren": {},
+  "_requested": {
+    "raw": "underscore@~1.7.0",
+    "scope": null,
+    "escapedName": "underscore",
+    "name": "underscore",
+    "rawSpec": "~1.7.0",
+    "spec": ">=1.7.0 <1.8.0",
+    "type": "range"
+  },
+  "_requiredBy": [
+    "/httpntlm"
+  ],
+  "_resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz",
+  "_shasum": "6bbaf0877500d36be34ecaa584e0db9fef035209",
+  "_shrinkwrap": null,
+  "_spec": "underscore@~1.7.0",
+  "_where": "/Users/fzy/project/koa2_Sequelize_project/node_modules/httpntlm",
+  "author": {
+    "name": "Jeremy Ashkenas",
+    "email": "jeremy@documentcloud.org"
+  },
+  "bugs": {
+    "url": "https://github.com/jashkenas/underscore/issues"
+  },
+  "dependencies": {},
+  "description": "JavaScript's functional programming helper library.",
+  "devDependencies": {
+    "docco": "0.6.x",
+    "eslint": "0.6.x",
+    "phantomjs": "1.9.7-1",
+    "uglify-js": "2.4.x"
+  },
+  "directories": {},
+  "dist": {
+    "shasum": "6bbaf0877500d36be34ecaa584e0db9fef035209",
+    "tarball": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz"
+  },
+  "files": [
+    "underscore.js",
+    "underscore-min.js",
+    "LICENSE"
+  ],
+  "gitHead": "da996e665deb0b69b257e80e3e257c04fde4191c",
+  "homepage": "http://underscorejs.org",
+  "keywords": [
+    "util",
+    "functional",
+    "server",
+    "client",
+    "browser"
+  ],
+  "licenses": [
+    {
+      "type": "MIT",
+      "url": "https://raw.github.com/jashkenas/underscore/master/LICENSE"
+    }
+  ],
+  "main": "underscore.js",
+  "maintainers": [
+    {
+      "name": "jashkenas",
+      "email": "jashkenas@gmail.com"
+    }
+  ],
+  "name": "underscore",
+  "optionalDependencies": {},
+  "readme": "                       __\n                      /\\ \\                                                         __\n     __  __    ___    \\_\\ \\     __   _ __   ____    ___    ___   _ __    __       /\\_\\    ____\n    /\\ \\/\\ \\ /' _ `\\  /'_  \\  /'__`\\/\\  __\\/ ,__\\  / ___\\ / __`\\/\\  __\\/'__`\\     \\/\\ \\  /',__\\\n    \\ \\ \\_\\ \\/\\ \\/\\ \\/\\ \\ \\ \\/\\  __/\\ \\ \\//\\__, `\\/\\ \\__//\\ \\ \\ \\ \\ \\//\\  __/  __  \\ \\ \\/\\__, `\\\n     \\ \\____/\\ \\_\\ \\_\\ \\___,_\\ \\____\\\\ \\_\\\\/\\____/\\ \\____\\ \\____/\\ \\_\\\\ \\____\\/\\_\\ _\\ \\ \\/\\____/\n      \\/___/  \\/_/\\/_/\\/__,_ /\\/____/ \\/_/ \\/___/  \\/____/\\/___/  \\/_/ \\/____/\\/_//\\ \\_\\ \\/___/\n                                                                                  \\ \\____/\n                                                                                   \\/___/\n\nUnderscore.js is a utility-belt library for JavaScript that provides\nsupport for the usual functional suspects (each, map, reduce, filter...)\nwithout extending any core JavaScript objects.\n\nFor Docs, License, Tests, and pre-packed downloads, see:\nhttp://underscorejs.org\n\nUnderscore is an open-sourced component of DocumentCloud:\nhttps://github.com/documentcloud\n\nMany thanks to our contributors:\nhttps://github.com/jashkenas/underscore/contributors\n",
+  "readmeFilename": "README.md",
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/jashkenas/underscore.git"
+  },
+  "scripts": {
+    "build": "uglifyjs underscore.js -c \"evaluate=false\" --comments \"/    .*/\" -m --source-map underscore-min.map -o underscore-min.js",
+    "doc": "docco underscore.js",
+    "test": "phantomjs test/vendor/runner.js test/index.html?noglobals=true && eslint underscore.js test/*.js test/vendor/runner.js"
+  },
+  "version": "1.7.0"
+}
diff --git a/node_modules/httpntlm/node_modules/underscore/underscore-min.js b/node_modules/httpntlm/node_modules/underscore/underscore-min.js
new file mode 100644
index 0000000..11f1d96
--- /dev/null
+++ b/node_modules/httpntlm/node_modules/underscore/underscore-min.js
@@ -0,0 +1,6 @@
+//     Underscore.js 1.7.0
+//     http://underscorejs.org
+//     (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+//     Underscore may be freely distributed under the MIT license.
+(function(){var n=this,t=n._,r=Array.prototype,e=Object.prototype,u=Function.prototype,i=r.push,a=r.slice,o=r.concat,l=e.toString,c=e.hasOwnProperty,f=Array.isArray,s=Object.keys,p=u.bind,h=function(n){return n instanceof h?n:this instanceof h?void(this._wrapped=n):new h(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=h),exports._=h):n._=h,h.VERSION="1.7.0";var g=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}};h.iteratee=function(n,t,r){return null==n?h.identity:h.isFunction(n)?g(n,t,r):h.isObject(n)?h.matches(n):h.property(n)},h.each=h.forEach=function(n,t,r){if(null==n)return n;t=g(t,r);var e,u=n.length;if(u===+u)for(e=0;u>e;e++)t(n[e],e,n);else{var i=h.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},h.map=h.collect=function(n,t,r){if(null==n)return[];t=h.iteratee(t,r);for(var e,u=n.length!==+n.length&&h.keys(n),i=(u||n).length,a=Array(i),o=0;i>o;o++)e=u?u[o]:o,a[o]=t(n[e],e,n);return a};var v="Reduce of empty array with no initial value";h.reduce=h.foldl=h.inject=function(n,t,r,e){null==n&&(n=[]),t=g(t,e,4);var u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length,o=0;if(arguments.length<3){if(!a)throw new TypeError(v);r=n[i?i[o++]:o++]}for(;a>o;o++)u=i?i[o]:o,r=t(r,n[u],u,n);return r},h.reduceRight=h.foldr=function(n,t,r,e){null==n&&(n=[]),t=g(t,e,4);var u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;if(arguments.length<3){if(!a)throw new TypeError(v);r=n[i?i[--a]:--a]}for(;a--;)u=i?i[a]:a,r=t(r,n[u],u,n);return r},h.find=h.detect=function(n,t,r){var e;return t=h.iteratee(t,r),h.some(n,function(n,r,u){return t(n,r,u)?(e=n,!0):void 0}),e},h.filter=h.select=function(n,t,r){var e=[];return null==n?e:(t=h.iteratee(t,r),h.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e)},h.reject=function(n,t,r){return h.filter(n,h.negate(h.iteratee(t)),r)},h.every=h.all=function(n,t,r){if(null==n)return!0;t=h.iteratee(t,r);var e,u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;for(e=0;a>e;e++)if(u=i?i[e]:e,!t(n[u],u,n))return!1;return!0},h.some=h.any=function(n,t,r){if(null==n)return!1;t=h.iteratee(t,r);var e,u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;for(e=0;a>e;e++)if(u=i?i[e]:e,t(n[u],u,n))return!0;return!1},h.contains=h.include=function(n,t){return null==n?!1:(n.length!==+n.length&&(n=h.values(n)),h.indexOf(n,t)>=0)},h.invoke=function(n,t){var r=a.call(arguments,2),e=h.isFunction(t);return h.map(n,function(n){return(e?t:n[t]).apply(n,r)})},h.pluck=function(n,t){return h.map(n,h.property(t))},h.where=function(n,t){return h.filter(n,h.matches(t))},h.findWhere=function(n,t){return h.find(n,h.matches(t))},h.max=function(n,t,r){var e,u,i=-1/0,a=-1/0;if(null==t&&null!=n){n=n.length===+n.length?n:h.values(n);for(var o=0,l=n.length;l>o;o++)e=n[o],e>i&&(i=e)}else t=h.iteratee(t,r),h.each(n,function(n,r,e){u=t(n,r,e),(u>a||u===-1/0&&i===-1/0)&&(i=n,a=u)});return i},h.min=function(n,t,r){var e,u,i=1/0,a=1/0;if(null==t&&null!=n){n=n.length===+n.length?n:h.values(n);for(var o=0,l=n.length;l>o;o++)e=n[o],i>e&&(i=e)}else t=h.iteratee(t,r),h.each(n,function(n,r,e){u=t(n,r,e),(a>u||1/0===u&&1/0===i)&&(i=n,a=u)});return i},h.shuffle=function(n){for(var t,r=n&&n.length===+n.length?n:h.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=h.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},h.sample=function(n,t,r){return null==t||r?(n.length!==+n.length&&(n=h.values(n)),n[h.random(n.length-1)]):h.shuffle(n).slice(0,Math.max(0,t))},h.sortBy=function(n,t,r){return t=h.iteratee(t,r),h.pluck(h.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var m=function(n){return function(t,r,e){var u={};return r=h.iteratee(r,e),h.each(t,function(e,i){var a=r(e,i,t);n(u,e,a)}),u}};h.groupBy=m(function(n,t,r){h.has(n,r)?n[r].push(t):n[r]=[t]}),h.indexBy=m(function(n,t,r){n[r]=t}),h.countBy=m(function(n,t,r){h.has(n,r)?n[r]++:n[r]=1}),h.sortedIndex=function(n,t,r,e){r=h.iteratee(r,e,1);for(var u=r(t),i=0,a=n.length;a>i;){var o=i+a>>>1;r(n[o])<u?i=o+1:a=o}return i},h.toArray=function(n){return n?h.isArray(n)?a.call(n):n.length===+n.length?h.map(n,h.identity):h.values(n):[]},h.size=function(n){return null==n?0:n.length===+n.length?n.length:h.keys(n).length},h.partition=function(n,t,r){t=h.iteratee(t,r);var e=[],u=[];return h.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},h.first=h.head=h.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:0>t?[]:a.call(n,0,t)},h.initial=function(n,t,r){return a.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},h.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:a.call(n,Math.max(n.length-t,0))},h.rest=h.tail=h.drop=function(n,t,r){return a.call(n,null==t||r?1:t)},h.compact=function(n){return h.filter(n,h.identity)};var y=function(n,t,r,e){if(t&&h.every(n,h.isArray))return o.apply(e,n);for(var u=0,a=n.length;a>u;u++){var l=n[u];h.isArray(l)||h.isArguments(l)?t?i.apply(e,l):y(l,t,r,e):r||e.push(l)}return e};h.flatten=function(n,t){return y(n,t,!1,[])},h.without=function(n){return h.difference(n,a.call(arguments,1))},h.uniq=h.unique=function(n,t,r,e){if(null==n)return[];h.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=h.iteratee(r,e));for(var u=[],i=[],a=0,o=n.length;o>a;a++){var l=n[a];if(t)a&&i===l||u.push(l),i=l;else if(r){var c=r(l,a,n);h.indexOf(i,c)<0&&(i.push(c),u.push(l))}else h.indexOf(u,l)<0&&u.push(l)}return u},h.union=function(){return h.uniq(y(arguments,!0,!0,[]))},h.intersection=function(n){if(null==n)return[];for(var t=[],r=arguments.length,e=0,u=n.length;u>e;e++){var i=n[e];if(!h.contains(t,i)){for(var a=1;r>a&&h.contains(arguments[a],i);a++);a===r&&t.push(i)}}return t},h.difference=function(n){var t=y(a.call(arguments,1),!0,!0,[]);return h.filter(n,function(n){return!h.contains(t,n)})},h.zip=function(n){if(null==n)return[];for(var t=h.max(arguments,"length").length,r=Array(t),e=0;t>e;e++)r[e]=h.pluck(arguments,e);return r},h.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},h.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=h.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}for(;u>e;e++)if(n[e]===t)return e;return-1},h.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=n.length;for("number"==typeof r&&(e=0>r?e+r+1:Math.min(e,r+1));--e>=0;)if(n[e]===t)return e;return-1},h.range=function(n,t,r){arguments.length<=1&&(t=n||0,n=0),r=r||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=Array(e),i=0;e>i;i++,n+=r)u[i]=n;return u};var d=function(){};h.bind=function(n,t){var r,e;if(p&&n.bind===p)return p.apply(n,a.call(arguments,1));if(!h.isFunction(n))throw new TypeError("Bind must be called on a function");return r=a.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(a.call(arguments)));d.prototype=n.prototype;var u=new d;d.prototype=null;var i=n.apply(u,r.concat(a.call(arguments)));return h.isObject(i)?i:u}},h.partial=function(n){var t=a.call(arguments,1);return function(){for(var r=0,e=t.slice(),u=0,i=e.length;i>u;u++)e[u]===h&&(e[u]=arguments[r++]);for(;r<arguments.length;)e.push(arguments[r++]);return n.apply(this,e)}},h.bindAll=function(n){var t,r,e=arguments.length;if(1>=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=h.bind(n[r],n);return n},h.memoize=function(n,t){var r=function(e){var u=r.cache,i=t?t.apply(this,arguments):e;return h.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},h.delay=function(n,t){var r=a.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},h.defer=function(n){return h.delay.apply(h,[n,1].concat(a.call(arguments,1)))},h.throttle=function(n,t,r){var e,u,i,a=null,o=0;r||(r={});var l=function(){o=r.leading===!1?0:h.now(),a=null,i=n.apply(e,u),a||(e=u=null)};return function(){var c=h.now();o||r.leading!==!1||(o=c);var f=t-(c-o);return e=this,u=arguments,0>=f||f>t?(clearTimeout(a),a=null,o=c,i=n.apply(e,u),a||(e=u=null)):a||r.trailing===!1||(a=setTimeout(l,f)),i}},h.debounce=function(n,t,r){var e,u,i,a,o,l=function(){var c=h.now()-a;t>c&&c>0?e=setTimeout(l,t-c):(e=null,r||(o=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,a=h.now();var c=r&&!e;return e||(e=setTimeout(l,t)),c&&(o=n.apply(i,u),i=u=null),o}},h.wrap=function(n,t){return h.partial(t,n)},h.negate=function(n){return function(){return!n.apply(this,arguments)}},h.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},h.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},h.before=function(n,t){var r;return function(){return--n>0?r=t.apply(this,arguments):t=null,r}},h.once=h.partial(h.before,2),h.keys=function(n){if(!h.isObject(n))return[];if(s)return s(n);var t=[];for(var r in n)h.has(n,r)&&t.push(r);return t},h.values=function(n){for(var t=h.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},h.pairs=function(n){for(var t=h.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},h.invert=function(n){for(var t={},r=h.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},h.functions=h.methods=function(n){var t=[];for(var r in n)h.isFunction(n[r])&&t.push(r);return t.sort()},h.extend=function(n){if(!h.isObject(n))return n;for(var t,r,e=1,u=arguments.length;u>e;e++){t=arguments[e];for(r in t)c.call(t,r)&&(n[r]=t[r])}return n},h.pick=function(n,t,r){var e,u={};if(null==n)return u;if(h.isFunction(t)){t=g(t,r);for(e in n){var i=n[e];t(i,e,n)&&(u[e]=i)}}else{var l=o.apply([],a.call(arguments,1));n=new Object(n);for(var c=0,f=l.length;f>c;c++)e=l[c],e in n&&(u[e]=n[e])}return u},h.omit=function(n,t,r){if(h.isFunction(t))t=h.negate(t);else{var e=h.map(o.apply([],a.call(arguments,1)),String);t=function(n,t){return!h.contains(e,t)}}return h.pick(n,t,r)},h.defaults=function(n){if(!h.isObject(n))return n;for(var t=1,r=arguments.length;r>t;t++){var e=arguments[t];for(var u in e)n[u]===void 0&&(n[u]=e[u])}return n},h.clone=function(n){return h.isObject(n)?h.isArray(n)?n.slice():h.extend({},n):n},h.tap=function(n,t){return t(n),n};var b=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof h&&(n=n._wrapped),t instanceof h&&(t=t._wrapped);var u=l.call(n);if(u!==l.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]===n)return e[i]===t;var a=n.constructor,o=t.constructor;if(a!==o&&"constructor"in n&&"constructor"in t&&!(h.isFunction(a)&&a instanceof a&&h.isFunction(o)&&o instanceof o))return!1;r.push(n),e.push(t);var c,f;if("[object Array]"===u){if(c=n.length,f=c===t.length)for(;c--&&(f=b(n[c],t[c],r,e)););}else{var s,p=h.keys(n);if(c=p.length,f=h.keys(t).length===c)for(;c--&&(s=p[c],f=h.has(t,s)&&b(n[s],t[s],r,e)););}return r.pop(),e.pop(),f};h.isEqual=function(n,t){return b(n,t,[],[])},h.isEmpty=function(n){if(null==n)return!0;if(h.isArray(n)||h.isString(n)||h.isArguments(n))return 0===n.length;for(var t in n)if(h.has(n,t))return!1;return!0},h.isElement=function(n){return!(!n||1!==n.nodeType)},h.isArray=f||function(n){return"[object Array]"===l.call(n)},h.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},h.each(["Arguments","Function","String","Number","Date","RegExp"],function(n){h["is"+n]=function(t){return l.call(t)==="[object "+n+"]"}}),h.isArguments(arguments)||(h.isArguments=function(n){return h.has(n,"callee")}),"function"!=typeof/./&&(h.isFunction=function(n){return"function"==typeof n||!1}),h.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},h.isNaN=function(n){return h.isNumber(n)&&n!==+n},h.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===l.call(n)},h.isNull=function(n){return null===n},h.isUndefined=function(n){return n===void 0},h.has=function(n,t){return null!=n&&c.call(n,t)},h.noConflict=function(){return n._=t,this},h.identity=function(n){return n},h.constant=function(n){return function(){return n}},h.noop=function(){},h.property=function(n){return function(t){return t[n]}},h.matches=function(n){var t=h.pairs(n),r=t.length;return function(n){if(null==n)return!r;n=new Object(n);for(var e=0;r>e;e++){var u=t[e],i=u[0];if(u[1]!==n[i]||!(i in n))return!1}return!0}},h.times=function(n,t,r){var e=Array(Math.max(0,n));t=g(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},h.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},h.now=Date.now||function(){return(new Date).getTime()};var _={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","`":"&#x60;"},w=h.invert(_),j=function(n){var t=function(t){return n[t]},r="(?:"+h.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};h.escape=j(_),h.unescape=j(w),h.result=function(n,t){if(null==n)return void 0;var r=n[t];return h.isFunction(r)?n[t]():r};var x=0;h.uniqueId=function(n){var t=++x+"";return n?n+t:t},h.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var A=/(.)^/,k={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},O=/\\|'|\r|\n|\u2028|\u2029/g,F=function(n){return"\\"+k[n]};h.template=function(n,t,r){!t&&r&&(t=r),t=h.defaults({},t,h.templateSettings);var e=RegExp([(t.escape||A).source,(t.interpolate||A).source,(t.evaluate||A).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,a,o){return i+=n.slice(u,o).replace(O,F),u=o+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":a&&(i+="';\n"+a+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var a=new Function(t.variable||"obj","_",i)}catch(o){throw o.source=i,o}var l=function(n){return a.call(this,n,h)},c=t.variable||"obj";return l.source="function("+c+"){\n"+i+"}",l},h.chain=function(n){var t=h(n);return t._chain=!0,t};var E=function(n){return this._chain?h(n).chain():n};h.mixin=function(n){h.each(h.functions(n),function(t){var r=h[t]=n[t];h.prototype[t]=function(){var n=[this._wrapped];return i.apply(n,arguments),E.call(this,r.apply(h,n))}})},h.mixin(h),h.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=r[n];h.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],E.call(this,r)}}),h.each(["concat","join","slice"],function(n){var t=r[n];h.prototype[n]=function(){return E.call(this,t.apply(this._wrapped,arguments))}}),h.prototype.value=function(){return this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return h})}).call(this);
+//# sourceMappingURL=underscore-min.map
\ No newline at end of file
diff --git a/node_modules/httpntlm/node_modules/underscore/underscore.js b/node_modules/httpntlm/node_modules/underscore/underscore.js
new file mode 100644
index 0000000..b4f49a0
--- /dev/null
+++ b/node_modules/httpntlm/node_modules/underscore/underscore.js
@@ -0,0 +1,1415 @@
+//     Underscore.js 1.7.0
+//     http://underscorejs.org
+//     (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+//     Underscore may be freely distributed under the MIT license.
+
+(function() {
+
+  // Baseline setup
+  // --------------
+
+  // Establish the root object, `window` in the browser, or `exports` on the server.
+  var root = this;
+
+  // Save the previous value of the `_` variable.
+  var previousUnderscore = root._;
+
+  // Save bytes in the minified (but not gzipped) version:
+  var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
+
+  // Create quick reference variables for speed access to core prototypes.
+  var
+    push             = ArrayProto.push,
+    slice            = ArrayProto.slice,
+    concat           = ArrayProto.concat,
+    toString         = ObjProto.toString,
+    hasOwnProperty   = ObjProto.hasOwnProperty;
+
+  // All **ECMAScript 5** native function implementations that we hope to use
+  // are declared here.
+  var
+    nativeIsArray      = Array.isArray,
+    nativeKeys         = Object.keys,
+    nativeBind         = FuncProto.bind;
+
+  // Create a safe reference to the Underscore object for use below.
+  var _ = function(obj) {
+    if (obj instanceof _) return obj;
+    if (!(this instanceof _)) return new _(obj);
+    this._wrapped = obj;
+  };
+
+  // Export the Underscore object for **Node.js**, with
+  // backwards-compatibility for the old `require()` API. If we're in
+  // the browser, add `_` as a global object.
+  if (typeof exports !== 'undefined') {
+    if (typeof module !== 'undefined' && module.exports) {
+      exports = module.exports = _;
+    }
+    exports._ = _;
+  } else {
+    root._ = _;
+  }
+
+  // Current version.
+  _.VERSION = '1.7.0';
+
+  // Internal function that returns an efficient (for current engines) version
+  // of the passed-in callback, to be repeatedly applied in other Underscore
+  // functions.
+  var createCallback = function(func, context, argCount) {
+    if (context === void 0) return func;
+    switch (argCount == null ? 3 : argCount) {
+      case 1: return function(value) {
+        return func.call(context, value);
+      };
+      case 2: return function(value, other) {
+        return func.call(context, value, other);
+      };
+      case 3: return function(value, index, collection) {
+        return func.call(context, value, index, collection);
+      };
+      case 4: return function(accumulator, value, index, collection) {
+        return func.call(context, accumulator, value, index, collection);
+      };
+    }
+    return function() {
+      return func.apply(context, arguments);
+    };
+  };
+
+  // A mostly-internal function to generate callbacks that can be applied
+  // to each element in a collection, returning the desired result — either
+  // identity, an arbitrary callback, a property matcher, or a property accessor.
+  _.iteratee = function(value, context, argCount) {
+    if (value == null) return _.identity;
+    if (_.isFunction(value)) return createCallback(value, context, argCount);
+    if (_.isObject(value)) return _.matches(value);
+    return _.property(value);
+  };
+
+  // Collection Functions
+  // --------------------
+
+  // The cornerstone, an `each` implementation, aka `forEach`.
+  // Handles raw objects in addition to array-likes. Treats all
+  // sparse array-likes as if they were dense.
+  _.each = _.forEach = function(obj, iteratee, context) {
+    if (obj == null) return obj;
+    iteratee = createCallback(iteratee, context);
+    var i, length = obj.length;
+    if (length === +length) {
+      for (i = 0; i < length; i++) {
+        iteratee(obj[i], i, obj);
+      }
+    } else {
+      var keys = _.keys(obj);
+      for (i = 0, length = keys.length; i < length; i++) {
+        iteratee(obj[keys[i]], keys[i], obj);
+      }
+    }
+    return obj;
+  };
+
+  // Return the results of applying the iteratee to each element.
+  _.map = _.collect = function(obj, iteratee, context) {
+    if (obj == null) return [];
+    iteratee = _.iteratee(iteratee, context);
+    var keys = obj.length !== +obj.length && _.keys(obj),
+        length = (keys || obj).length,
+        results = Array(length),
+        currentKey;
+    for (var index = 0; index < length; index++) {
+      currentKey = keys ? keys[index] : index;
+      results[index] = iteratee(obj[currentKey], currentKey, obj);
+    }
+    return results;
+  };
+
+  var reduceError = 'Reduce of empty array with no initial value';
+
+  // **Reduce** builds up a single result from a list of values, aka `inject`,
+  // or `foldl`.
+  _.reduce = _.foldl = _.inject = function(obj, iteratee, memo, context) {
+    if (obj == null) obj = [];
+    iteratee = createCallback(iteratee, context, 4);
+    var keys = obj.length !== +obj.length && _.keys(obj),
+        length = (keys || obj).length,
+        index = 0, currentKey;
+    if (arguments.length < 3) {
+      if (!length) throw new TypeError(reduceError);
+      memo = obj[keys ? keys[index++] : index++];
+    }
+    for (; index < length; index++) {
+      currentKey = keys ? keys[index] : index;
+      memo = iteratee(memo, obj[currentKey], currentKey, obj);
+    }
+    return memo;
+  };
+
+  // The right-associative version of reduce, also known as `foldr`.
+  _.reduceRight = _.foldr = function(obj, iteratee, memo, context) {
+    if (obj == null) obj = [];
+    iteratee = createCallback(iteratee, context, 4);
+    var keys = obj.length !== + obj.length && _.keys(obj),
+        index = (keys || obj).length,
+        currentKey;
+    if (arguments.length < 3) {
+      if (!index) throw new TypeError(reduceError);
+      memo = obj[keys ? keys[--index] : --index];
+    }
+    while (index--) {
+      currentKey = keys ? keys[index] : index;
+      memo = iteratee(memo, obj[currentKey], currentKey, obj);
+    }
+    return memo;
+  };
+
+  // Return the first value which passes a truth test. Aliased as `detect`.
+  _.find = _.detect = function(obj, predicate, context) {
+    var result;
+    predicate = _.iteratee(predicate, context);
+    _.some(obj, function(value, index, list) {
+      if (predicate(value, index, list)) {
+        result = value;
+        return true;
+      }
+    });
+    return result;
+  };
+
+  // Return all the elements that pass a truth test.
+  // Aliased as `select`.
+  _.filter = _.select = function(obj, predicate, context) {
+    var results = [];
+    if (obj == null) return results;
+    predicate = _.iteratee(predicate, context);
+    _.each(obj, function(value, index, list) {
+      if (predicate(value, index, list)) results.push(value);
+    });
+    return results;
+  };
+
+  // Return all the elements for which a truth test fails.
+  _.reject = function(obj, predicate, context) {
+    return _.filter(obj, _.negate(_.iteratee(predicate)), context);
+  };
+
+  // Determine whether all of the elements match a truth test.
+  // Aliased as `all`.
+  _.every = _.all = function(obj, predicate, context) {
+    if (obj == null) return true;
+    predicate = _.iteratee(predicate, context);
+    var keys = obj.length !== +obj.length && _.keys(obj),
+        length = (keys || obj).length,
+        index, currentKey;
+    for (index = 0; index < length; index++) {
+      currentKey = keys ? keys[index] : index;
+      if (!predicate(obj[currentKey], currentKey, obj)) return false;
+    }
+    return true;
+  };
+
+  // Determine if at least one element in the object matches a truth test.
+  // Aliased as `any`.
+  _.some = _.any = function(obj, predicate, context) {
+    if (obj == null) return false;
+    predicate = _.iteratee(predicate, context);
+    var keys = obj.length !== +obj.length && _.keys(obj),
+        length = (keys || obj).length,
+        index, currentKey;
+    for (index = 0; index < length; index++) {
+      currentKey = keys ? keys[index] : index;
+      if (predicate(obj[currentKey], currentKey, obj)) return true;
+    }
+    return false;
+  };
+
+  // Determine if the array or object contains a given value (using `===`).
+  // Aliased as `include`.
+  _.contains = _.include = function(obj, target) {
+    if (obj == null) return false;
+    if (obj.length !== +obj.length) obj = _.values(obj);
+    return _.indexOf(obj, target) >= 0;
+  };
+
+  // Invoke a method (with arguments) on every item in a collection.
+  _.invoke = function(obj, method) {
+    var args = slice.call(arguments, 2);
+    var isFunc = _.isFunction(method);
+    return _.map(obj, function(value) {
+      return (isFunc ? method : value[method]).apply(value, args);
+    });
+  };
+
+  // Convenience version of a common use case of `map`: fetching a property.
+  _.pluck = function(obj, key) {
+    return _.map(obj, _.property(key));
+  };
+
+  // Convenience version of a common use case of `filter`: selecting only objects
+  // containing specific `key:value` pairs.
+  _.where = function(obj, attrs) {
+    return _.filter(obj, _.matches(attrs));
+  };
+
+  // Convenience version of a common use case of `find`: getting the first object
+  // containing specific `key:value` pairs.
+  _.findWhere = function(obj, attrs) {
+    return _.find(obj, _.matches(attrs));
+  };
+
+  // Return the maximum element (or element-based computation).
+  _.max = function(obj, iteratee, context) {
+    var result = -Infinity, lastComputed = -Infinity,
+        value, computed;
+    if (iteratee == null && obj != null) {
+      obj = obj.length === +obj.length ? obj : _.values(obj);
+      for (var i = 0, length = obj.length; i < length; i++) {
+        value = obj[i];
+        if (value > result) {
+          result = value;
+        }
+      }
+    } else {
+      iteratee = _.iteratee(iteratee, context);
+      _.each(obj, function(value, index, list) {
+        computed = iteratee(value, index, list);
+        if (computed > lastComputed || computed === -Infinity && result === -Infinity) {
+          result = value;
+          lastComputed = computed;
+        }
+      });
+    }
+    return result;
+  };
+
+  // Return the minimum element (or element-based computation).
+  _.min = function(obj, iteratee, context) {
+    var result = Infinity, lastComputed = Infinity,
+        value, computed;
+    if (iteratee == null && obj != null) {
+      obj = obj.length === +obj.length ? obj : _.values(obj);
+      for (var i = 0, length = obj.length; i < length; i++) {
+        value = obj[i];
+        if (value < result) {
+          result = value;
+        }
+      }
+    } else {
+      iteratee = _.iteratee(iteratee, context);
+      _.each(obj, function(value, index, list) {
+        computed = iteratee(value, index, list);
+        if (computed < lastComputed || computed === Infinity && result === Infinity) {
+          result = value;
+          lastComputed = computed;
+        }
+      });
+    }
+    return result;
+  };
+
+  // Shuffle a collection, using the modern version of the
+  // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).
+  _.shuffle = function(obj) {
+    var set = obj && obj.length === +obj.length ? obj : _.values(obj);
+    var length = set.length;
+    var shuffled = Array(length);
+    for (var index = 0, rand; index < length; index++) {
+      rand = _.random(0, index);
+      if (rand !== index) shuffled[index] = shuffled[rand];
+      shuffled[rand] = set[index];
+    }
+    return shuffled;
+  };
+
+  // Sample **n** random values from a collection.
+  // If **n** is not specified, returns a single random element.
+  // The internal `guard` argument allows it to work with `map`.
+  _.sample = function(obj, n, guard) {
+    if (n == null || guard) {
+      if (obj.length !== +obj.length) obj = _.values(obj);
+      return obj[_.random(obj.length - 1)];
+    }
+    return _.shuffle(obj).slice(0, Math.max(0, n));
+  };
+
+  // Sort the object's values by a criterion produced by an iteratee.
+  _.sortBy = function(obj, iteratee, context) {
+    iteratee = _.iteratee(iteratee, context);
+    return _.pluck(_.map(obj, function(value, index, list) {
+      return {
+        value: value,
+        index: index,
+        criteria: iteratee(value, index, list)
+      };
+    }).sort(function(left, right) {
+      var a = left.criteria;
+      var b = right.criteria;
+      if (a !== b) {
+        if (a > b || a === void 0) return 1;
+        if (a < b || b === void 0) return -1;
+      }
+      return left.index - right.index;
+    }), 'value');
+  };
+
+  // An internal function used for aggregate "group by" operations.
+  var group = function(behavior) {
+    return function(obj, iteratee, context) {
+      var result = {};
+      iteratee = _.iteratee(iteratee, context);
+      _.each(obj, function(value, index) {
+        var key = iteratee(value, index, obj);
+        behavior(result, value, key);
+      });
+      return result;
+    };
+  };
+
+  // Groups the object's values by a criterion. Pass either a string attribute
+  // to group by, or a function that returns the criterion.
+  _.groupBy = group(function(result, value, key) {
+    if (_.has(result, key)) result[key].push(value); else result[key] = [value];
+  });
+
+  // Indexes the object's values by a criterion, similar to `groupBy`, but for
+  // when you know that your index values will be unique.
+  _.indexBy = group(function(result, value, key) {
+    result[key] = value;
+  });
+
+  // Counts instances of an object that group by a certain criterion. Pass
+  // either a string attribute to count by, or a function that returns the
+  // criterion.
+  _.countBy = group(function(result, value, key) {
+    if (_.has(result, key)) result[key]++; else result[key] = 1;
+  });
+
+  // Use a comparator function to figure out the smallest index at which
+  // an object should be inserted so as to maintain order. Uses binary search.
+  _.sortedIndex = function(array, obj, iteratee, context) {
+    iteratee = _.iteratee(iteratee, context, 1);
+    var value = iteratee(obj);
+    var low = 0, high = array.length;
+    while (low < high) {
+      var mid = low + high >>> 1;
+      if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;
+    }
+    return low;
+  };
+
+  // Safely create a real, live array from anything iterable.
+  _.toArray = function(obj) {
+    if (!obj) return [];
+    if (_.isArray(obj)) return slice.call(obj);
+    if (obj.length === +obj.length) return _.map(obj, _.identity);
+    return _.values(obj);
+  };
+
+  // Return the number of elements in an object.
+  _.size = function(obj) {
+    if (obj == null) return 0;
+    return obj.length === +obj.length ? obj.length : _.keys(obj).length;
+  };
+
+  // Split a collection into two arrays: one whose elements all satisfy the given
+  // predicate, and one whose elements all do not satisfy the predicate.
+  _.partition = function(obj, predicate, context) {
+    predicate = _.iteratee(predicate, context);
+    var pass = [], fail = [];
+    _.each(obj, function(value, key, obj) {
+      (predicate(value, key, obj) ? pass : fail).push(value);
+    });
+    return [pass, fail];
+  };
+
+  // Array Functions
+  // ---------------
+
+  // Get the first element of an array. Passing **n** will return the first N
+  // values in the array. Aliased as `head` and `take`. The **guard** check
+  // allows it to work with `_.map`.
+  _.first = _.head = _.take = function(array, n, guard) {
+    if (array == null) return void 0;
+    if (n == null || guard) return array[0];
+    if (n < 0) return [];
+    return slice.call(array, 0, n);
+  };
+
+  // Returns everything but the last entry of the array. Especially useful on
+  // the arguments object. Passing **n** will return all the values in
+  // the array, excluding the last N. The **guard** check allows it to work with
+  // `_.map`.
+  _.initial = function(array, n, guard) {
+    return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n)));
+  };
+
+  // Get the last element of an array. Passing **n** will return the last N
+  // values in the array. The **guard** check allows it to work with `_.map`.
+  _.last = function(array, n, guard) {
+    if (array == null) return void 0;
+    if (n == null || guard) return array[array.length - 1];
+    return slice.call(array, Math.max(array.length - n, 0));
+  };
+
+  // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
+  // Especially useful on the arguments object. Passing an **n** will return
+  // the rest N values in the array. The **guard**
+  // check allows it to work with `_.map`.
+  _.rest = _.tail = _.drop = function(array, n, guard) {
+    return slice.call(array, n == null || guard ? 1 : n);
+  };
+
+  // Trim out all falsy values from an array.
+  _.compact = function(array) {
+    return _.filter(array, _.identity);
+  };
+
+  // Internal implementation of a recursive `flatten` function.
+  var flatten = function(input, shallow, strict, output) {
+    if (shallow && _.every(input, _.isArray)) {
+      return concat.apply(output, input);
+    }
+    for (var i = 0, length = input.length; i < length; i++) {
+      var value = input[i];
+      if (!_.isArray(value) && !_.isArguments(value)) {
+        if (!strict) output.push(value);
+      } else if (shallow) {
+        push.apply(output, value);
+      } else {
+        flatten(value, shallow, strict, output);
+      }
+    }
+    return output;
+  };
+
+  // Flatten out an array, either recursively (by default), or just one level.
+  _.flatten = function(array, shallow) {
+    return flatten(array, shallow, false, []);
+  };
+
+  // Return a version of the array that does not contain the specified value(s).
+  _.without = function(array) {
+    return _.difference(array, slice.call(arguments, 1));
+  };
+
+  // Produce a duplicate-free version of the array. If the array has already
+  // been sorted, you have the option of using a faster algorithm.
+  // Aliased as `unique`.
+  _.uniq = _.unique = function(array, isSorted, iteratee, context) {
+    if (array == null) return [];
+    if (!_.isBoolean(isSorted)) {
+      context = iteratee;
+      iteratee = isSorted;
+      isSorted = false;
+    }
+    if (iteratee != null) iteratee = _.iteratee(iteratee, context);
+    var result = [];
+    var seen = [];
+    for (var i = 0, length = array.length; i < length; i++) {
+      var value = array[i];
+      if (isSorted) {
+        if (!i || seen !== value) result.push(value);
+        seen = value;
+      } else if (iteratee) {
+        var computed = iteratee(value, i, array);
+        if (_.indexOf(seen, computed) < 0) {
+          seen.push(computed);
+          result.push(value);
+        }
+      } else if (_.indexOf(result, value) < 0) {
+        result.push(value);
+      }
+    }
+    return result;
+  };
+
+  // Produce an array that contains the union: each distinct element from all of
+  // the passed-in arrays.
+  _.union = function() {
+    return _.uniq(flatten(arguments, true, true, []));
+  };
+
+  // Produce an array that contains every item shared between all the
+  // passed-in arrays.
+  _.intersection = function(array) {
+    if (array == null) return [];
+    var result = [];
+    var argsLength = arguments.length;
+    for (var i = 0, length = array.length; i < length; i++) {
+      var item = array[i];
+      if (_.contains(result, item)) continue;
+      for (var j = 1; j < argsLength; j++) {
+        if (!_.contains(arguments[j], item)) break;
+      }
+      if (j === argsLength) result.push(item);
+    }
+    return result;
+  };
+
+  // Take the difference between one array and a number of other arrays.
+  // Only the elements present in just the first array will remain.
+  _.difference = function(array) {
+    var rest = flatten(slice.call(arguments, 1), true, true, []);
+    return _.filter(array, function(value){
+      return !_.contains(rest, value);
+    });
+  };
+
+  // Zip together multiple lists into a single array -- elements that share
+  // an index go together.
+  _.zip = function(array) {
+    if (array == null) return [];
+    var length = _.max(arguments, 'length').length;
+    var results = Array(length);
+    for (var i = 0; i < length; i++) {
+      results[i] = _.pluck(arguments, i);
+    }
+    return results;
+  };
+
+  // Converts lists into objects. Pass either a single array of `[key, value]`
+  // pairs, or two parallel arrays of the same length -- one of keys, and one of
+  // the corresponding values.
+  _.object = function(list, values) {
+    if (list == null) return {};
+    var result = {};
+    for (var i = 0, length = list.length; i < length; i++) {
+      if (values) {
+        result[list[i]] = values[i];
+      } else {
+        result[list[i][0]] = list[i][1];
+      }
+    }
+    return result;
+  };
+
+  // Return the position of the first occurrence of an item in an array,
+  // or -1 if the item is not included in the array.
+  // If the array is large and already in sort order, pass `true`
+  // for **isSorted** to use binary search.
+  _.indexOf = function(array, item, isSorted) {
+    if (array == null) return -1;
+    var i = 0, length = array.length;
+    if (isSorted) {
+      if (typeof isSorted == 'number') {
+        i = isSorted < 0 ? Math.max(0, length + isSorted) : isSorted;
+      } else {
+        i = _.sortedIndex(array, item);
+        return array[i] === item ? i : -1;
+      }
+    }
+    for (; i < length; i++) if (array[i] === item) return i;
+    return -1;
+  };
+
+  _.lastIndexOf = function(array, item, from) {
+    if (array == null) return -1;
+    var idx = array.length;
+    if (typeof from == 'number') {
+      idx = from < 0 ? idx + from + 1 : Math.min(idx, from + 1);
+    }
+    while (--idx >= 0) if (array[idx] === item) return idx;
+    return -1;
+  };
+
+  // Generate an integer Array containing an arithmetic progression. A port of
+  // the native Python `range()` function. See
+  // [the Python documentation](http://docs.python.org/library/functions.html#range).
+  _.range = function(start, stop, step) {
+    if (arguments.length <= 1) {
+      stop = start || 0;
+      start = 0;
+    }
+    step = step || 1;
+
+    var length = Math.max(Math.ceil((stop - start) / step), 0);
+    var range = Array(length);
+
+    for (var idx = 0; idx < length; idx++, start += step) {
+      range[idx] = start;
+    }
+
+    return range;
+  };
+
+  // Function (ahem) Functions
+  // ------------------
+
+  // Reusable constructor function for prototype setting.
+  var Ctor = function(){};
+
+  // Create a function bound to a given object (assigning `this`, and arguments,
+  // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
+  // available.
+  _.bind = function(func, context) {
+    var args, bound;
+    if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
+    if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
+    args = slice.call(arguments, 2);
+    bound = function() {
+      if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
+      Ctor.prototype = func.prototype;
+      var self = new Ctor;
+      Ctor.prototype = null;
+      var result = func.apply(self, args.concat(slice.call(arguments)));
+      if (_.isObject(result)) return result;
+      return self;
+    };
+    return bound;
+  };
+
+  // Partially apply a function by creating a version that has had some of its
+  // arguments pre-filled, without changing its dynamic `this` context. _ acts
+  // as a placeholder, allowing any combination of arguments to be pre-filled.
+  _.partial = function(func) {
+    var boundArgs = slice.call(arguments, 1);
+    return function() {
+      var position = 0;
+      var args = boundArgs.slice();
+      for (var i = 0, length = args.length; i < length; i++) {
+        if (args[i] === _) args[i] = arguments[position++];
+      }
+      while (position < arguments.length) args.push(arguments[position++]);
+      return func.apply(this, args);
+    };
+  };
+
+  // Bind a number of an object's methods to that object. Remaining arguments
+  // are the method names to be bound. Useful for ensuring that all callbacks
+  // defined on an object belong to it.
+  _.bindAll = function(obj) {
+    var i, length = arguments.length, key;
+    if (length <= 1) throw new Error('bindAll must be passed function names');
+    for (i = 1; i < length; i++) {
+      key = arguments[i];
+      obj[key] = _.bind(obj[key], obj);
+    }
+    return obj;
+  };
+
+  // Memoize an expensive function by storing its results.
+  _.memoize = function(func, hasher) {
+    var memoize = function(key) {
+      var cache = memoize.cache;
+      var address = hasher ? hasher.apply(this, arguments) : key;
+      if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);
+      return cache[address];
+    };
+    memoize.cache = {};
+    return memoize;
+  };
+
+  // Delays a function for the given number of milliseconds, and then calls
+  // it with the arguments supplied.
+  _.delay = function(func, wait) {
+    var args = slice.call(arguments, 2);
+    return setTimeout(function(){
+      return func.apply(null, args);
+    }, wait);
+  };
+
+  // Defers a function, scheduling it to run after the current call stack has
+  // cleared.
+  _.defer = function(func) {
+    return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
+  };
+
+  // Returns a function, that, when invoked, will only be triggered at most once
+  // during a given window of time. Normally, the throttled function will run
+  // as much as it can, without ever going more than once per `wait` duration;
+  // but if you'd like to disable the execution on the leading edge, pass
+  // `{leading: false}`. To disable execution on the trailing edge, ditto.
+  _.throttle = function(func, wait, options) {
+    var context, args, result;
+    var timeout = null;
+    var previous = 0;
+    if (!options) options = {};
+    var later = function() {
+      previous = options.leading === false ? 0 : _.now();
+      timeout = null;
+      result = func.apply(context, args);
+      if (!timeout) context = args = null;
+    };
+    return function() {
+      var now = _.now();
+      if (!previous && options.leading === false) previous = now;
+      var remaining = wait - (now - previous);
+      context = this;
+      args = arguments;
+      if (remaining <= 0 || remaining > wait) {
+        clearTimeout(timeout);
+        timeout = null;
+        previous = now;
+        result = func.apply(context, args);
+        if (!timeout) context = args = null;
+      } else if (!timeout && options.trailing !== false) {
+        timeout = setTimeout(later, remaining);
+      }
+      return result;
+    };
+  };
+
+  // Returns a function, that, as long as it continues to be invoked, will not
+  // be triggered. The function will be called after it stops being called for
+  // N milliseconds. If `immediate` is passed, trigger the function on the
+  // leading edge, instead of the trailing.
+  _.debounce = function(func, wait, immediate) {
+    var timeout, args, context, timestamp, result;
+
+    var later = function() {
+      var last = _.now() - timestamp;
+
+      if (last < wait && last > 0) {
+        timeout = setTimeout(later, wait - last);
+      } else {
+        timeout = null;
+        if (!immediate) {
+          result = func.apply(context, args);
+          if (!timeout) context = args = null;
+        }
+      }
+    };
+
+    return function() {
+      context = this;
+      args = arguments;
+      timestamp = _.now();
+      var callNow = immediate && !timeout;
+      if (!timeout) timeout = setTimeout(later, wait);
+      if (callNow) {
+        result = func.apply(context, args);
+        context = args = null;
+      }
+
+      return result;
+    };
+  };
+
+  // Returns the first function passed as an argument to the second,
+  // allowing you to adjust arguments, run code before and after, and
+  // conditionally execute the original function.
+  _.wrap = function(func, wrapper) {
+    return _.partial(wrapper, func);
+  };
+
+  // Returns a negated version of the passed-in predicate.
+  _.negate = function(predicate) {
+    return function() {
+      return !predicate.apply(this, arguments);
+    };
+  };
+
+  // Returns a function that is the composition of a list of functions, each
+  // consuming the return value of the function that follows.
+  _.compose = function() {
+    var args = arguments;
+    var start = args.length - 1;
+    return function() {
+      var i = start;
+      var result = args[start].apply(this, arguments);
+      while (i--) result = args[i].call(this, result);
+      return result;
+    };
+  };
+
+  // Returns a function that will only be executed after being called N times.
+  _.after = function(times, func) {
+    return function() {
+      if (--times < 1) {
+        return func.apply(this, arguments);
+      }
+    };
+  };
+
+  // Returns a function that will only be executed before being called N times.
+  _.before = function(times, func) {
+    var memo;
+    return function() {
+      if (--times > 0) {
+        memo = func.apply(this, arguments);
+      } else {
+        func = null;
+      }
+      return memo;
+    };
+  };
+
+  // Returns a function that will be executed at most one time, no matter how
+  // often you call it. Useful for lazy initialization.
+  _.once = _.partial(_.before, 2);
+
+  // Object Functions
+  // ----------------
+
+  // Retrieve the names of an object's properties.
+  // Delegates to **ECMAScript 5**'s native `Object.keys`
+  _.keys = function(obj) {
+    if (!_.isObject(obj)) return [];
+    if (nativeKeys) return nativeKeys(obj);
+    var keys = [];
+    for (var key in obj) if (_.has(obj, key)) keys.push(key);
+    return keys;
+  };
+
+  // Retrieve the values of an object's properties.
+  _.values = function(obj) {
+    var keys = _.keys(obj);
+    var length = keys.length;
+    var values = Array(length);
+    for (var i = 0; i < length; i++) {
+      values[i] = obj[keys[i]];
+    }
+    return values;
+  };
+
+  // Convert an object into a list of `[key, value]` pairs.
+  _.pairs = function(obj) {
+    var keys = _.keys(obj);
+    var length = keys.length;
+    var pairs = Array(length);
+    for (var i = 0; i < length; i++) {
+      pairs[i] = [keys[i], obj[keys[i]]];
+    }
+    return pairs;
+  };
+
+  // Invert the keys and values of an object. The values must be serializable.
+  _.invert = function(obj) {
+    var result = {};
+    var keys = _.keys(obj);
+    for (var i = 0, length = keys.length; i < length; i++) {
+      result[obj[keys[i]]] = keys[i];
+    }
+    return result;
+  };
+
+  // Return a sorted list of the function names available on the object.
+  // Aliased as `methods`
+  _.functions = _.methods = function(obj) {
+    var names = [];
+    for (var key in obj) {
+      if (_.isFunction(obj[key])) names.push(key);
+    }
+    return names.sort();
+  };
+
+  // Extend a given object with all the properties in passed-in object(s).
+  _.extend = function(obj) {
+    if (!_.isObject(obj)) return obj;
+    var source, prop;
+    for (var i = 1, length = arguments.length; i < length; i++) {
+      source = arguments[i];
+      for (prop in source) {
+        if (hasOwnProperty.call(source, prop)) {
+            obj[prop] = source[prop];
+        }
+      }
+    }
+    return obj;
+  };
+
+  // Return a copy of the object only containing the whitelisted properties.
+  _.pick = function(obj, iteratee, context) {
+    var result = {}, key;
+    if (obj == null) return result;
+    if (_.isFunction(iteratee)) {
+      iteratee = createCallback(iteratee, context);
+      for (key in obj) {
+        var value = obj[key];
+        if (iteratee(value, key, obj)) result[key] = value;
+      }
+    } else {
+      var keys = concat.apply([], slice.call(arguments, 1));
+      obj = new Object(obj);
+      for (var i = 0, length = keys.length; i < length; i++) {
+        key = keys[i];
+        if (key in obj) result[key] = obj[key];
+      }
+    }
+    return result;
+  };
+
+   // Return a copy of the object without the blacklisted properties.
+  _.omit = function(obj, iteratee, context) {
+    if (_.isFunction(iteratee)) {
+      iteratee = _.negate(iteratee);
+    } else {
+      var keys = _.map(concat.apply([], slice.call(arguments, 1)), String);
+      iteratee = function(value, key) {
+        return !_.contains(keys, key);
+      };
+    }
+    return _.pick(obj, iteratee, context);
+  };
+
+  // Fill in a given object with default properties.
+  _.defaults = function(obj) {
+    if (!_.isObject(obj)) return obj;
+    for (var i = 1, length = arguments.length; i < length; i++) {
+      var source = arguments[i];
+      for (var prop in source) {
+        if (obj[prop] === void 0) obj[prop] = source[prop];
+      }
+    }
+    return obj;
+  };
+
+  // Create a (shallow-cloned) duplicate of an object.
+  _.clone = function(obj) {
+    if (!_.isObject(obj)) return obj;
+    return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
+  };
+
+  // Invokes interceptor with the obj, and then returns obj.
+  // The primary purpose of this method is to "tap into" a method chain, in
+  // order to perform operations on intermediate results within the chain.
+  _.tap = function(obj, interceptor) {
+    interceptor(obj);
+    return obj;
+  };
+
+  // Internal recursive comparison function for `isEqual`.
+  var eq = function(a, b, aStack, bStack) {
+    // Identical objects are equal. `0 === -0`, but they aren't identical.
+    // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
+    if (a === b) return a !== 0 || 1 / a === 1 / b;
+    // A strict comparison is necessary because `null == undefined`.
+    if (a == null || b == null) return a === b;
+    // Unwrap any wrapped objects.
+    if (a instanceof _) a = a._wrapped;
+    if (b instanceof _) b = b._wrapped;
+    // Compare `[[Class]]` names.
+    var className = toString.call(a);
+    if (className !== toString.call(b)) return false;
+    switch (className) {
+      // Strings, numbers, regular expressions, dates, and booleans are compared by value.
+      case '[object RegExp]':
+      // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
+      case '[object String]':
+        // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
+        // equivalent to `new String("5")`.
+        return '' + a === '' + b;
+      case '[object Number]':
+        // `NaN`s are equivalent, but non-reflexive.
+        // Object(NaN) is equivalent to NaN
+        if (+a !== +a) return +b !== +b;
+        // An `egal` comparison is performed for other numeric values.
+        return +a === 0 ? 1 / +a === 1 / b : +a === +b;
+      case '[object Date]':
+      case '[object Boolean]':
+        // Coerce dates and booleans to numeric primitive values. Dates are compared by their
+        // millisecond representations. Note that invalid dates with millisecond representations
+        // of `NaN` are not equivalent.
+        return +a === +b;
+    }
+    if (typeof a != 'object' || typeof b != 'object') return false;
+    // Assume equality for cyclic structures. The algorithm for detecting cyclic
+    // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
+    var length = aStack.length;
+    while (length--) {
+      // Linear search. Performance is inversely proportional to the number of
+      // unique nested structures.
+      if (aStack[length] === a) return bStack[length] === b;
+    }
+    // Objects with different constructors are not equivalent, but `Object`s
+    // from different frames are.
+    var aCtor = a.constructor, bCtor = b.constructor;
+    if (
+      aCtor !== bCtor &&
+      // Handle Object.create(x) cases
+      'constructor' in a && 'constructor' in b &&
+      !(_.isFunction(aCtor) && aCtor instanceof aCtor &&
+        _.isFunction(bCtor) && bCtor instanceof bCtor)
+    ) {
+      return false;
+    }
+    // Add the first object to the stack of traversed objects.
+    aStack.push(a);
+    bStack.push(b);
+    var size, result;
+    // Recursively compare objects and arrays.
+    if (className === '[object Array]') {
+      // Compare array lengths to determine if a deep comparison is necessary.
+      size = a.length;
+      result = size === b.length;
+      if (result) {
+        // Deep compare the contents, ignoring non-numeric properties.
+        while (size--) {
+          if (!(result = eq(a[size], b[size], aStack, bStack))) break;
+        }
+      }
+    } else {
+      // Deep compare objects.
+      var keys = _.keys(a), key;
+      size = keys.length;
+      // Ensure that both objects contain the same number of properties before comparing deep equality.
+      result = _.keys(b).length === size;
+      if (result) {
+        while (size--) {
+          // Deep compare each member
+          key = keys[size];
+          if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
+        }
+      }
+    }
+    // Remove the first object from the stack of traversed objects.
+    aStack.pop();
+    bStack.pop();
+    return result;
+  };
+
+  // Perform a deep comparison to check if two objects are equal.
+  _.isEqual = function(a, b) {
+    return eq(a, b, [], []);
+  };
+
+  // Is a given array, string, or object empty?
+  // An "empty" object has no enumerable own-properties.
+  _.isEmpty = function(obj) {
+    if (obj == null) return true;
+    if (_.isArray(obj) || _.isString(obj) || _.isArguments(obj)) return obj.length === 0;
+    for (var key in obj) if (_.has(obj, key)) return false;
+    return true;
+  };
+
+  // Is a given value a DOM element?
+  _.isElement = function(obj) {
+    return !!(obj && obj.nodeType === 1);
+  };
+
+  // Is a given value an array?
+  // Delegates to ECMA5's native Array.isArray
+  _.isArray = nativeIsArray || function(obj) {
+    return toString.call(obj) === '[object Array]';
+  };
+
+  // Is a given variable an object?
+  _.isObject = function(obj) {
+    var type = typeof obj;
+    return type === 'function' || type === 'object' && !!obj;
+  };
+
+  // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.
+  _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {
+    _['is' + name] = function(obj) {
+      return toString.call(obj) === '[object ' + name + ']';
+    };
+  });
+
+  // Define a fallback version of the method in browsers (ahem, IE), where
+  // there isn't any inspectable "Arguments" type.
+  if (!_.isArguments(arguments)) {
+    _.isArguments = function(obj) {
+      return _.has(obj, 'callee');
+    };
+  }
+
+  // Optimize `isFunction` if appropriate. Work around an IE 11 bug.
+  if (typeof /./ !== 'function') {
+    _.isFunction = function(obj) {
+      return typeof obj == 'function' || false;
+    };
+  }
+
+  // Is a given object a finite number?
+  _.isFinite = function(obj) {
+    return isFinite(obj) && !isNaN(parseFloat(obj));
+  };
+
+  // Is the given value `NaN`? (NaN is the only number which does not equal itself).
+  _.isNaN = function(obj) {
+    return _.isNumber(obj) && obj !== +obj;
+  };
+
+  // Is a given value a boolean?
+  _.isBoolean = function(obj) {
+    return obj === true || obj === false || toString.call(obj) === '[object Boolean]';
+  };
+
+  // Is a given value equal to null?
+  _.isNull = function(obj) {
+    return obj === null;
+  };
+
+  // Is a given variable undefined?
+  _.isUndefined = function(obj) {
+    return obj === void 0;
+  };
+
+  // Shortcut function for checking if an object has a given property directly
+  // on itself (in other words, not on a prototype).
+  _.has = function(obj, key) {
+    return obj != null && hasOwnProperty.call(obj, key);
+  };
+
+  // Utility Functions
+  // -----------------
+
+  // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
+  // previous owner. Returns a reference to the Underscore object.
+  _.noConflict = function() {
+    root._ = previousUnderscore;
+    return this;
+  };
+
+  // Keep the identity function around for default iteratees.
+  _.identity = function(value) {
+    return value;
+  };
+
+  _.constant = function(value) {
+    return function() {
+      return value;
+    };
+  };
+
+  _.noop = function(){};
+
+  _.property = function(key) {
+    return function(obj) {
+      return obj[key];
+    };
+  };
+
+  // Returns a predicate for checking whether an object has a given set of `key:value` pairs.
+  _.matches = function(attrs) {
+    var pairs = _.pairs(attrs), length = pairs.length;
+    return function(obj) {
+      if (obj == null) return !length;
+      obj = new Object(obj);
+      for (var i = 0; i < length; i++) {
+        var pair = pairs[i], key = pair[0];
+        if (pair[1] !== obj[key] || !(key in obj)) return false;
+      }
+      return true;
+    };
+  };
+
+  // Run a function **n** times.
+  _.times = function(n, iteratee, context) {
+    var accum = Array(Math.max(0, n));
+    iteratee = createCallback(iteratee, context, 1);
+    for (var i = 0; i < n; i++) accum[i] = iteratee(i);
+    return accum;
+  };
+
+  // Return a random integer between min and max (inclusive).
+  _.random = function(min, max) {
+    if (max == null) {
+      max = min;
+      min = 0;
+    }
+    return min + Math.floor(Math.random() * (max - min + 1));
+  };
+
+  // A (possibly faster) way to get the current timestamp as an integer.
+  _.now = Date.now || function() {
+    return new Date().getTime();
+  };
+
+   // List of HTML entities for escaping.
+  var escapeMap = {
+    '&': '&amp;',
+    '<': '&lt;',
+    '>': '&gt;',
+    '"': '&quot;',
+    "'": '&#x27;',
+    '`': '&#x60;'
+  };
+  var unescapeMap = _.invert(escapeMap);
+
+  // Functions for escaping and unescaping strings to/from HTML interpolation.
+  var createEscaper = function(map) {
+    var escaper = function(match) {
+      return map[match];
+    };
+    // Regexes for identifying a key that needs to be escaped
+    var source = '(?:' + _.keys(map).join('|') + ')';
+    var testRegexp = RegExp(source);
+    var replaceRegexp = RegExp(source, 'g');
+    return function(string) {
+      string = string == null ? '' : '' + string;
+      return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
+    };
+  };
+  _.escape = createEscaper(escapeMap);
+  _.unescape = createEscaper(unescapeMap);
+
+  // If the value of the named `property` is a function then invoke it with the
+  // `object` as context; otherwise, return it.
+  _.result = function(object, property) {
+    if (object == null) return void 0;
+    var value = object[property];
+    return _.isFunction(value) ? object[property]() : value;
+  };
+
+  // Generate a unique integer id (unique within the entire client session).
+  // Useful for temporary DOM ids.
+  var idCounter = 0;
+  _.uniqueId = function(prefix) {
+    var id = ++idCounter + '';
+    return prefix ? prefix + id : id;
+  };
+
+  // By default, Underscore uses ERB-style template delimiters, change the
+  // following template settings to use alternative delimiters.
+  _.templateSettings = {
+    evaluate    : /<%([\s\S]+?)%>/g,
+    interpolate : /<%=([\s\S]+?)%>/g,
+    escape      : /<%-([\s\S]+?)%>/g
+  };
+
+  // When customizing `templateSettings`, if you don't want to define an
+  // interpolation, evaluation or escaping regex, we need one that is
+  // guaranteed not to match.
+  var noMatch = /(.)^/;
+
+  // Certain characters need to be escaped so that they can be put into a
+  // string literal.
+  var escapes = {
+    "'":      "'",
+    '\\':     '\\',
+    '\r':     'r',
+    '\n':     'n',
+    '\u2028': 'u2028',
+    '\u2029': 'u2029'
+  };
+
+  var escaper = /\\|'|\r|\n|\u2028|\u2029/g;
+
+  var escapeChar = function(match) {
+    return '\\' + escapes[match];
+  };
+
+  // JavaScript micro-templating, similar to John Resig's implementation.
+  // Underscore templating handles arbitrary delimiters, preserves whitespace,
+  // and correctly escapes quotes within interpolated code.
+  // NB: `oldSettings` only exists for backwards compatibility.
+  _.template = function(text, settings, oldSettings) {
+    if (!settings && oldSettings) settings = oldSettings;
+    settings = _.defaults({}, settings, _.templateSettings);
+
+    // Combine delimiters into one regular expression via alternation.
+    var matcher = RegExp([
+      (settings.escape || noMatch).source,
+      (settings.interpolate || noMatch).source,
+      (settings.evaluate || noMatch).source
+    ].join('|') + '|$', 'g');
+
+    // Compile the template source, escaping string literals appropriately.
+    var index = 0;
+    var source = "__p+='";
+    text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
+      source += text.slice(index, offset).replace(escaper, escapeChar);
+      index = offset + match.length;
+
+      if (escape) {
+        source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
+      } else if (interpolate) {
+        source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
+      } else if (evaluate) {
+        source += "';\n" + evaluate + "\n__p+='";
+      }
+
+      // Adobe VMs need the match returned to produce the correct offest.
+      return match;
+    });
+    source += "';\n";
+
+    // If a variable is not specified, place data values in local scope.
+    if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
+
+    source = "var __t,__p='',__j=Array.prototype.join," +
+      "print=function(){__p+=__j.call(arguments,'');};\n" +
+      source + 'return __p;\n';
+
+    try {
+      var render = new Function(settings.variable || 'obj', '_', source);
+    } catch (e) {
+      e.source = source;
+      throw e;
+    }
+
+    var template = function(data) {
+      return render.call(this, data, _);
+    };
+
+    // Provide the compiled source as a convenience for precompilation.
+    var argument = settings.variable || 'obj';
+    template.source = 'function(' + argument + '){\n' + source + '}';
+
+    return template;
+  };
+
+  // Add a "chain" function. Start chaining a wrapped Underscore object.
+  _.chain = function(obj) {
+    var instance = _(obj);
+    instance._chain = true;
+    return instance;
+  };
+
+  // OOP
+  // ---------------
+  // If Underscore is called as a function, it returns a wrapped object that
+  // can be used OO-style. This wrapper holds altered versions of all the
+  // underscore functions. Wrapped objects may be chained.
+
+  // Helper function to continue chaining intermediate results.
+  var result = function(obj) {
+    return this._chain ? _(obj).chain() : obj;
+  };
+
+  // Add your own custom functions to the Underscore object.
+  _.mixin = function(obj) {
+    _.each(_.functions(obj), function(name) {
+      var func = _[name] = obj[name];
+      _.prototype[name] = function() {
+        var args = [this._wrapped];
+        push.apply(args, arguments);
+        return result.call(this, func.apply(_, args));
+      };
+    });
+  };
+
+  // Add all of the Underscore functions to the wrapper object.
+  _.mixin(_);
+
+  // Add all mutator Array functions to the wrapper.
+  _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
+    var method = ArrayProto[name];
+    _.prototype[name] = function() {
+      var obj = this._wrapped;
+      method.apply(obj, arguments);
+      if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
+      return result.call(this, obj);
+    };
+  });
+
+  // Add all accessor Array functions to the wrapper.
+  _.each(['concat', 'join', 'slice'], function(name) {
+    var method = ArrayProto[name];
+    _.prototype[name] = function() {
+      return result.call(this, method.apply(this._wrapped, arguments));
+    };
+  });
+
+  // Extracts the result from a wrapped and chained object.
+  _.prototype.value = function() {
+    return this._wrapped;
+  };
+
+  // AMD registration happens at the end for compatibility with AMD loaders
+  // that may not enforce next-turn semantics on modules. Even though general
+  // practice for AMD registration is to be anonymous, underscore registers
+  // as a named module because, like jQuery, it is a base library that is
+  // popular enough to be bundled in a third party lib, but not be part of
+  // an AMD load request. Those cases could generate an error when an
+  // anonymous define() is called outside of a loader request.
+  if (typeof define === 'function' && define.amd) {
+    define('underscore', [], function() {
+      return _;
+    });
+  }
+}.call(this));
diff --git a/node_modules/httpntlm/ntlm.js b/node_modules/httpntlm/ntlm.js
new file mode 100644
index 0000000..5a776c3
--- /dev/null
+++ b/node_modules/httpntlm/ntlm.js
@@ -0,0 +1,390 @@
+var crypto = require('crypto');
+
+var flags = {
+	NTLM_NegotiateUnicode                :  0x00000001,
+	NTLM_NegotiateOEM                    :  0x00000002,
+	NTLM_RequestTarget                   :  0x00000004,
+	NTLM_Unknown9                        :  0x00000008,
+	NTLM_NegotiateSign                   :  0x00000010,
+	NTLM_NegotiateSeal                   :  0x00000020,
+	NTLM_NegotiateDatagram               :  0x00000040,
+	NTLM_NegotiateLanManagerKey          :  0x00000080,
+	NTLM_Unknown8                        :  0x00000100,
+	NTLM_NegotiateNTLM                   :  0x00000200,
+	NTLM_NegotiateNTOnly                 :  0x00000400,
+	NTLM_Anonymous                       :  0x00000800,
+	NTLM_NegotiateOemDomainSupplied      :  0x00001000,
+	NTLM_NegotiateOemWorkstationSupplied :  0x00002000,
+	NTLM_Unknown6                        :  0x00004000,
+	NTLM_NegotiateAlwaysSign             :  0x00008000,
+	NTLM_TargetTypeDomain                :  0x00010000,
+	NTLM_TargetTypeServer                :  0x00020000,
+	NTLM_TargetTypeShare                 :  0x00040000,
+	NTLM_NegotiateExtendedSecurity       :  0x00080000,
+	NTLM_NegotiateIdentify               :  0x00100000,
+	NTLM_Unknown5                        :  0x00200000,
+	NTLM_RequestNonNTSessionKey          :  0x00400000,
+	NTLM_NegotiateTargetInfo             :  0x00800000,
+	NTLM_Unknown4                        :  0x01000000,
+	NTLM_NegotiateVersion                :  0x02000000,
+	NTLM_Unknown3                        :  0x04000000,
+	NTLM_Unknown2                        :  0x08000000,
+	NTLM_Unknown1                        :  0x10000000,
+	NTLM_Negotiate128                    :  0x20000000,
+	NTLM_NegotiateKeyExchange            :  0x40000000,
+	NTLM_Negotiate56                     :  0x80000000
+};
+var typeflags = {
+	NTLM_TYPE1_FLAGS : 	  flags.NTLM_NegotiateUnicode
+						+ flags.NTLM_NegotiateOEM
+						+ flags.NTLM_RequestTarget
+						+ flags.NTLM_NegotiateNTLM
+						+ flags.NTLM_NegotiateOemDomainSupplied
+						+ flags.NTLM_NegotiateOemWorkstationSupplied
+						+ flags.NTLM_NegotiateAlwaysSign
+						+ flags.NTLM_NegotiateExtendedSecurity
+						+ flags.NTLM_NegotiateVersion
+						+ flags.NTLM_Negotiate128
+						+ flags.NTLM_Negotiate56,
+
+	NTLM_TYPE2_FLAGS :    flags.NTLM_NegotiateUnicode
+						+ flags.NTLM_RequestTarget
+						+ flags.NTLM_NegotiateNTLM
+						+ flags.NTLM_NegotiateAlwaysSign
+						+ flags.NTLM_NegotiateExtendedSecurity
+						+ flags.NTLM_NegotiateTargetInfo
+						+ flags.NTLM_NegotiateVersion
+						+ flags.NTLM_Negotiate128
+						+ flags.NTLM_Negotiate56
+};
+
+function createType1Message(options){
+	var domain = escape(options.domain.toUpperCase());
+	var workstation = escape(options.workstation.toUpperCase());
+	var protocol = 'NTLMSSP\0';
+
+	var BODY_LENGTH = 40;
+
+	var type1flags = typeflags.NTLM_TYPE1_FLAGS;
+	if(!domain || domain === '')
+		type1flags = type1flags - flags.NTLM_NegotiateOemDomainSupplied;
+
+	var pos = 0;
+	var buf = new Buffer(BODY_LENGTH + domain.length + workstation.length);
+
+
+	buf.write(protocol, pos, protocol.length); pos += protocol.length; // protocol
+	buf.writeUInt32LE(1, pos); pos += 4;          // type 1
+	buf.writeUInt32LE(type1flags, pos); pos += 4; // TYPE1 flag
+
+	buf.writeUInt16LE(domain.length, pos); pos += 2; // domain length
+	buf.writeUInt16LE(domain.length, pos); pos += 2; // domain max length
+	buf.writeUInt32LE(BODY_LENGTH + workstation.length, pos); pos += 4; // domain buffer offset
+
+	buf.writeUInt16LE(workstation.length, pos); pos += 2; // workstation length
+	buf.writeUInt16LE(workstation.length, pos); pos += 2; // workstation max length
+	buf.writeUInt32LE(BODY_LENGTH, pos); pos += 4; // workstation buffer offset
+
+	buf.writeUInt8(5, pos); pos += 1;      //ProductMajorVersion
+	buf.writeUInt8(1, pos); pos += 1;      //ProductMinorVersion
+	buf.writeUInt16LE(2600, pos); pos += 2; //ProductBuild
+
+	buf.writeUInt8(0 , pos); pos += 1; //VersionReserved1
+	buf.writeUInt8(0 , pos); pos += 1; //VersionReserved2
+	buf.writeUInt8(0 , pos); pos += 1; //VersionReserved3
+	buf.writeUInt8(15, pos); pos += 1; //NTLMRevisionCurrent
+
+	buf.write(workstation, pos, workstation.length, 'ascii'); pos += workstation.length; // workstation string
+	buf.write(domain     , pos, domain.length     , 'ascii'); pos += domain.length;
+
+	return 'NTLM ' + buf.toString('base64');
+}
+
+function parseType2Message(rawmsg, callback){
+	var match = rawmsg.match(/NTLM (.+)?/);
+	if(!match || !match[1])
+		return callback(new Error("Couldn't find NTLM in the message type2 comming from the server"));
+
+	var buf = new Buffer(match[1], 'base64');
+
+	var msg = {};
+
+	msg.signature = buf.slice(0, 8);
+	msg.type = buf.readInt16LE(8);
+
+	if(msg.type != 2)
+		return callback(new Error("Server didn't return a type 2 message"));
+
+	msg.targetNameLen = buf.readInt16LE(12);
+	msg.targetNameMaxLen = buf.readInt16LE(14);
+	msg.targetNameOffset = buf.readInt32LE(16);
+	msg.targetName  = buf.slice(msg.targetNameOffset, msg.targetNameOffset + msg.targetNameMaxLen);
+
+    msg.negotiateFlags = buf.readInt32LE(20);
+    msg.serverChallenge = buf.slice(24, 32);
+    msg.reserved = buf.slice(32, 40);
+
+    if(msg.negotiateFlags & flags.NTLM_NegotiateTargetInfo){
+    	msg.targetInfoLen = buf.readInt16LE(40);
+    	msg.targetInfoMaxLen = buf.readInt16LE(42);
+    	msg.targetInfoOffset = buf.readInt32LE(44);
+    	msg.targetInfo = buf.slice(msg.targetInfoOffset, msg.targetInfoOffset + msg.targetInfoLen);
+    }
+	return msg;
+}
+
+function createType3Message(msg2, options){
+	var nonce = msg2.serverChallenge;
+	var username = options.username;
+	var password = options.password;
+	var negotiateFlags = msg2.negotiateFlags;
+
+	var isUnicode = negotiateFlags & flags.NTLM_NegotiateUnicode;
+	var isNegotiateExtendedSecurity = negotiateFlags & flags.NTLM_NegotiateExtendedSecurity;
+
+	var BODY_LENGTH = 72;
+
+	var domainName = escape(options.domain.toUpperCase());
+	var workstation = escape(options.workstation.toUpperCase());
+
+	var workstationBytes, domainNameBytes, usernameBytes, encryptedRandomSessionKeyBytes;
+
+	var encryptedRandomSessionKey = "";
+	if(isUnicode){
+		workstationBytes = new Buffer(workstation, 'utf16le');
+		domainNameBytes = new Buffer(domainName, 'utf16le');
+		usernameBytes = new Buffer(username, 'utf16le');
+		encryptedRandomSessionKeyBytes = new Buffer(encryptedRandomSessionKey, 'utf16le');
+	}else{
+		workstationBytes = new Buffer(workstation, 'ascii');
+		domainNameBytes = new Buffer(domainName, 'ascii');
+		usernameBytes = new Buffer(username, 'ascii');
+		encryptedRandomSessionKeyBytes = new Buffer(encryptedRandomSessionKey, 'ascii');
+	}
+
+	var lmChallengeResponse = calc_resp(create_LM_hashed_password_v1(password), nonce);
+	var ntChallengeResponse = calc_resp(create_NT_hashed_password_v1(password), nonce);
+
+	if(isNegotiateExtendedSecurity){
+		var pwhash = create_NT_hashed_password_v1(password);
+	 	var clientChallenge = "";
+	 	for(var i=0; i < 8; i++){
+	 		clientChallenge += String.fromCharCode( Math.floor(Math.random()*256) );
+	   	}
+	   	var clientChallengeBytes = new Buffer(clientChallenge, 'ascii');
+	    var challenges = ntlm2sr_calc_resp(pwhash, nonce, clientChallengeBytes);
+	    lmChallengeResponse = challenges.lmChallengeResponse;
+	    ntChallengeResponse = challenges.ntChallengeResponse;
+	}
+
+	var signature = 'NTLMSSP\0';
+
+	var pos = 0;
+	var buf = new Buffer(BODY_LENGTH + domainNameBytes.length + usernameBytes.length + workstationBytes.length + lmChallengeResponse.length + ntChallengeResponse.length + encryptedRandomSessionKeyBytes.length);
+
+	buf.write(signature, pos, signature.length); pos += signature.length;
+	buf.writeUInt32LE(3, pos); pos += 4;          // type 1
+
+	buf.writeUInt16LE(lmChallengeResponse.length, pos); pos += 2; // LmChallengeResponseLen
+	buf.writeUInt16LE(lmChallengeResponse.length, pos); pos += 2; // LmChallengeResponseMaxLen
+	buf.writeUInt32LE(BODY_LENGTH + domainNameBytes.length + usernameBytes.length + workstationBytes.length, pos); pos += 4; // LmChallengeResponseOffset
+
+	buf.writeUInt16LE(ntChallengeResponse.length, pos); pos += 2; // NtChallengeResponseLen
+	buf.writeUInt16LE(ntChallengeResponse.length, pos); pos += 2; // NtChallengeResponseMaxLen
+	buf.writeUInt32LE(BODY_LENGTH + domainNameBytes.length + usernameBytes.length + workstationBytes.length + lmChallengeResponse.length, pos); pos += 4; // NtChallengeResponseOffset
+
+	buf.writeUInt16LE(domainNameBytes.length, pos); pos += 2; // DomainNameLen
+	buf.writeUInt16LE(domainNameBytes.length, pos); pos += 2; // DomainNameMaxLen
+	buf.writeUInt32LE(BODY_LENGTH, pos); pos += 4; 			  // DomainNameOffset
+
+	buf.writeUInt16LE(usernameBytes.length, pos); pos += 2; // UserNameLen
+	buf.writeUInt16LE(usernameBytes.length, pos); pos += 2; // UserNameMaxLen
+	buf.writeUInt32LE(BODY_LENGTH + domainNameBytes.length, pos); pos += 4; // UserNameOffset
+
+	buf.writeUInt16LE(workstationBytes.length, pos); pos += 2; // WorkstationLen
+	buf.writeUInt16LE(workstationBytes.length, pos); pos += 2; // WorkstationMaxLen
+	buf.writeUInt32LE(BODY_LENGTH + domainNameBytes.length + usernameBytes.length, pos); pos += 4; // WorkstationOffset
+
+	buf.writeUInt16LE(encryptedRandomSessionKeyBytes.length, pos); pos += 2; // EncryptedRandomSessionKeyLen
+	buf.writeUInt16LE(encryptedRandomSessionKeyBytes.length, pos); pos += 2; // EncryptedRandomSessionKeyMaxLen
+	buf.writeUInt32LE(BODY_LENGTH + domainNameBytes.length + usernameBytes.length + workstationBytes.length + lmChallengeResponse.length + ntChallengeResponse.length, pos); pos += 4; // EncryptedRandomSessionKeyOffset
+
+	buf.writeUInt32LE(typeflags.NTLM_TYPE2_FLAGS, pos); pos += 4; // NegotiateFlags
+
+	buf.writeUInt8(5, pos); pos++; // ProductMajorVersion
+	buf.writeUInt8(1, pos); pos++; // ProductMinorVersion
+	buf.writeUInt16LE(2600, pos); pos += 2; // ProductBuild
+	buf.writeUInt8(0, pos); pos++; // VersionReserved1
+	buf.writeUInt8(0, pos); pos++; // VersionReserved2
+	buf.writeUInt8(0, pos); pos++; // VersionReserved3
+	buf.writeUInt8(15, pos); pos++; // NTLMRevisionCurrent
+
+	domainNameBytes.copy(buf, pos); pos += domainNameBytes.length;
+	usernameBytes.copy(buf, pos); pos += usernameBytes.length;
+	workstationBytes.copy(buf, pos); pos += workstationBytes.length;
+	lmChallengeResponse.copy(buf, pos); pos += lmChallengeResponse.length;
+	ntChallengeResponse.copy(buf, pos); pos += ntChallengeResponse.length;
+	encryptedRandomSessionKeyBytes.copy(buf, pos); pos += encryptedRandomSessionKeyBytes.length;
+
+	return 'NTLM ' + buf.toString('base64');
+}
+
+function create_LM_hashed_password_v1(password){
+	// fix the password length to 14 bytes
+	password = password.toUpperCase();
+	var passwordBytes = new Buffer(password, 'ascii');
+
+	var passwordBytesPadded = new Buffer(14);
+	passwordBytesPadded.fill("\0");
+	var sourceEnd = 14;
+	if(passwordBytes.length < 14) sourceEnd = passwordBytes.length;
+	passwordBytes.copy(passwordBytesPadded, 0, 0, sourceEnd);
+
+	// split into 2 parts of 7 bytes:
+	var firstPart = passwordBytesPadded.slice(0,7);
+	var secondPart = passwordBytesPadded.slice(7);
+
+	function encrypt(buf){
+		var key = insertZerosEvery7Bits(buf);
+		var des = crypto.createCipheriv('DES-ECB', key, '');
+		return des.update("KGS!@#$%"); // page 57 in [MS-NLMP]);
+	}
+
+	var firstPartEncrypted = encrypt(firstPart);
+	var secondPartEncrypted = encrypt(secondPart);
+
+	return Buffer.concat([firstPartEncrypted, secondPartEncrypted]);
+}
+
+function insertZerosEvery7Bits(buf){
+	var binaryArray = bytes2binaryArray(buf);
+	var newBinaryArray = [];
+	for(var i=0; i<binaryArray.length; i++){
+		newBinaryArray.push(binaryArray[i]);
+
+		if((i+1)%7 === 0){
+			newBinaryArray.push(0);
+		}
+	}
+	return binaryArray2bytes(newBinaryArray);
+}
+
+function bytes2binaryArray(buf){
+	var hex2binary = {
+		0: [0,0,0,0],
+		1: [0,0,0,1],
+		2: [0,0,1,0],
+		3: [0,0,1,1],
+		4: [0,1,0,0],
+		5: [0,1,0,1],
+		6: [0,1,1,0],
+		7: [0,1,1,1],
+		8: [1,0,0,0],
+		9: [1,0,0,1],
+		A: [1,0,1,0],
+		B: [1,0,1,1],
+		C: [1,1,0,0],
+		D: [1,1,0,1],
+		E: [1,1,1,0],
+		F: [1,1,1,1]
+	};
+
+	var hexString = buf.toString('hex').toUpperCase();
+	var array = [];
+	for(var i=0; i<hexString.length; i++){
+   		var hexchar = hexString.charAt(i);
+   		array = array.concat(hex2binary[hexchar]);
+   	}
+   	return array;
+}
+
+function binaryArray2bytes(array){
+	var binary2hex = {
+		'0000': 0,
+		'0001': 1,
+		'0010': 2,
+		'0011': 3,
+		'0100': 4,
+		'0101': 5,
+		'0110': 6,
+		'0111': 7,
+		'1000': 8,
+		'1001': 9,
+		'1010': 'A',
+		'1011': 'B',
+		'1100': 'C',
+		'1101': 'D',
+		'1110': 'E',
+		'1111': 'F'
+	};
+
+ 	var bufArray = [];
+
+	for(var i=0; i<array.length; i +=8 ){
+		if((i+7) > array.length)
+			break;
+
+		var binString1 = '' + array[i] + '' + array[i+1] + '' + array[i+2] + '' + array[i+3];
+		var binString2 = '' + array[i+4] + '' + array[i+5] + '' + array[i+6] + '' + array[i+7];
+   		var hexchar1 = binary2hex[binString1];
+   		var hexchar2 = binary2hex[binString2];
+
+   		var buf = new Buffer(hexchar1 + '' + hexchar2, 'hex');
+   		bufArray.push(buf);
+   	}
+
+   	return Buffer.concat(bufArray);
+}
+
+function create_NT_hashed_password_v1(password){
+	var buf = new Buffer(password, 'utf16le');
+	var md4 = crypto.createHash('md4');
+	md4.update(buf);
+	return new Buffer(md4.digest());
+}
+
+function calc_resp(password_hash, server_challenge){
+    // padding with zeros to make the hash 21 bytes long
+    var passHashPadded = new Buffer(21);
+    passHashPadded.fill("\0");
+    password_hash.copy(passHashPadded, 0, 0, password_hash.length);
+
+    var resArray = [];
+
+    var des = crypto.createCipheriv('DES-ECB', insertZerosEvery7Bits(passHashPadded.slice(0,7)), '');
+    resArray.push( des.update(server_challenge.slice(0,8)) );
+
+    des = crypto.createCipheriv('DES-ECB', insertZerosEvery7Bits(passHashPadded.slice(7,14)), '');
+    resArray.push( des.update(server_challenge.slice(0,8)) );
+
+    des = crypto.createCipheriv('DES-ECB', insertZerosEvery7Bits(passHashPadded.slice(14,21)), '');
+    resArray.push( des.update(server_challenge.slice(0,8)) );
+
+   	return Buffer.concat(resArray);
+}
+
+function ntlm2sr_calc_resp(responseKeyNT, serverChallenge, clientChallenge){
+	// padding with zeros to make the hash 16 bytes longer
+    var lmChallengeResponse = new Buffer(clientChallenge.length + 16);
+    lmChallengeResponse.fill("\0");
+    clientChallenge.copy(lmChallengeResponse, 0, 0, clientChallenge.length);
+
+    var buf = Buffer.concat([serverChallenge, clientChallenge]);
+    var md5 = crypto.createHash('md5');
+    md5.update(buf);
+    var sess = md5.digest();
+    var ntChallengeResponse = calc_resp(responseKeyNT, sess.slice(0,8));
+
+    return {
+    	lmChallengeResponse: lmChallengeResponse,
+    	ntChallengeResponse: ntChallengeResponse
+    };
+}
+
+exports.createType1Message = createType1Message;
+exports.parseType2Message = parseType2Message;
+exports.createType3Message = createType3Message;
+
+
+
+
diff --git a/node_modules/httpntlm/package.json b/node_modules/httpntlm/package.json
new file mode 100644
index 0000000..b17872c
--- /dev/null
+++ b/node_modules/httpntlm/package.json
@@ -0,0 +1,96 @@
+{
+  "_args": [
+    [
+      {
+        "raw": "httpntlm@1.6.1",
+        "scope": null,
+        "escapedName": "httpntlm",
+        "name": "httpntlm",
+        "rawSpec": "1.6.1",
+        "spec": "1.6.1",
+        "type": "version"
+      },
+      "/Users/fzy/project/koa2_Sequelize_project/node_modules/smtp-connection"
+    ]
+  ],
+  "_from": "httpntlm@1.6.1",
+  "_id": "httpntlm@1.6.1",
+  "_inCache": true,
+  "_location": "/httpntlm",
+  "_nodeVersion": "0.12.2",
+  "_npmOperationalInternal": {
+    "host": "packages-16-east.internal.npmjs.com",
+    "tmp": "tmp/httpntlm-1.6.1.tgz_1462189866942_0.9128970899619162"
+  },
+  "_npmUser": {
+    "name": "samdecrock",
+    "email": "sam.decrock@gmail.com"
+  },
+  "_npmVersion": "2.7.4",
+  "_phantomChildren": {},
+  "_requested": {
+    "raw": "httpntlm@1.6.1",
+    "scope": null,
+    "escapedName": "httpntlm",
+    "name": "httpntlm",
+    "rawSpec": "1.6.1",
+    "spec": "1.6.1",
+    "type": "version"
+  },
+  "_requiredBy": [
+    "/smtp-connection"
+  ],
+  "_resolved": "https://registry.npmjs.org/httpntlm/-/httpntlm-1.6.1.tgz",
+  "_shasum": "ad01527143a2e8773cfae6a96f58656bb52a34b2",
+  "_shrinkwrap": null,
+  "_spec": "httpntlm@1.6.1",
+  "_where": "/Users/fzy/project/koa2_Sequelize_project/node_modules/smtp-connection",
+  "author": {
+    "name": "Sam Decrock",
+    "url": "https://github.com/SamDecrock/"
+  },
+  "bugs": {
+    "url": "https://github.com/SamDecrock/node-http-ntlm/issues"
+  },
+  "dependencies": {
+    "httpreq": ">=0.4.22",
+    "underscore": "~1.7.0"
+  },
+  "description": "httpntlm is a Node.js library to do HTTP NTLM authentication",
+  "devDependencies": {},
+  "directories": {},
+  "dist": {
+    "shasum": "ad01527143a2e8773cfae6a96f58656bb52a34b2",
+    "tarball": "https://registry.npmjs.org/httpntlm/-/httpntlm-1.6.1.tgz"
+  },
+  "engines": {
+    "node": ">=0.8.0"
+  },
+  "gitHead": "c5cecb5a94ef1fd33d5234aec3fad8c59b920eb2",
+  "homepage": "https://github.com/SamDecrock/node-http-ntlm#readme",
+  "licenses": [
+    {
+      "type": "MIT",
+      "url": "http://www.opensource.org/licenses/mit-license.php"
+    }
+  ],
+  "main": "./httpntlm",
+  "maintainers": [
+    {
+      "name": "samdecrock",
+      "email": "sam.decrock@gmail.com"
+    }
+  ],
+  "name": "httpntlm",
+  "optionalDependencies": {},
+  "readme": "# httpntlm\n\n__httpntlm__ is a Node.js library to do HTTP NTLM authentication\n\nIt's a port from the Python libary [python-ntml](https://code.google.com/p/python-ntlm/)\n\n## Install\n\nYou can install __httpntlm__ using the Node Package Manager (npm):\n\n    npm install httpntlm\n\n## How to use\n\n```js\nvar httpntlm = require('httpntlm');\n\nhttpntlm.get({\n    url: \"https://someurl.com\",\n    username: 'm$',\n    password: 'stinks',\n    workstation: 'choose.something',\n    domain: ''\n}, function (err, res){\n    if(err) return err;\n\n    console.log(res.headers);\n    console.log(res.body);\n});\n```\n\nIt supports __http__ and __https__.\n\n## Options\n\n- `url:`      _{String}_   URL to connect. (Required)\n- `username:` _{String}_   Username. (Required)\n- `password:` _{String}_   Password. (Required)\n- `workstation:` _{String}_ Name of workstation or `''`.\n- `domain:`   _{String}_   Name of domain or `''`.\n\nYou can also pass along all other options of [httpreq](https://github.com/SamDecrock/node-httpreq), including custom headers, cookies, body data, ... and use POST, PUT or DELETE instead of GET.\n\n## Advanced\n\nIf you want to use the NTLM-functions yourself, you can access the ntlm-library like this (https example):\n\n```js\nvar ntlm = require('httpntlm').ntlm;\nvar async = require('async');\nvar httpreq = require('httpreq');\nvar HttpsAgent = require('agentkeepalive').HttpsAgent;\nvar keepaliveAgent = new HttpsAgent();\n\nvar options = {\n    url: \"https://someurl.com\",\n    username: 'm$',\n    password: 'stinks',\n    workstation: 'choose.something',\n    domain: ''\n};\n\nasync.waterfall([\n    function (callback){\n        var type1msg = ntlm.createType1Message(options);\n\n        httpreq.get(options.url, {\n            headers:{\n                'Connection' : 'keep-alive',\n                'Authorization': type1msg\n            },\n            agent: keepaliveAgent\n        }, callback);\n    },\n\n    function (res, callback){\n        if(!res.headers['www-authenticate'])\n            return callback(new Error('www-authenticate not found on response of second request'));\n\n        var type2msg = ntlm.parseType2Message(res.headers['www-authenticate']);\n        var type3msg = ntlm.createType3Message(type2msg, options);\n\n        setImmediate(function() {\n            httpreq.get(options.url, {\n                headers:{\n                    'Connection' : 'Close',\n                    'Authorization': type3msg\n                },\n                allowRedirects: false,\n                agent: keepaliveAgent\n            }, callback);\n        });\n    }\n], function (err, res) {\n    if(err) return console.log(err);\n\n    console.log(res.headers);\n    console.log(res.body);\n});\n```\n\n## Download binary files\n\n```javascript\nhttpntlm.get({\n    url: \"https://someurl.com/file.xls\",\n    username: 'm$',\n    password: 'stinks',\n    workstation: 'choose.something',\n    domain: '',\n    binary: true\n}, function (err, response) {\n    if(err) return console.log(err);\n    fs.writeFile(\"file.xls\", response.body, function (err) {\n        if(err) return console.log(\"error writing file\");\n        console.log(\"file.xls saved!\");\n    });\n});\n```\n\n## More information\n\n* [python-ntlm](https://code.google.com/p/python-ntlm/)\n* [NTLM Authentication Scheme for HTTP](http://www.innovation.ch/personal/ronald/ntlm.html)\n* [LM hash on Wikipedia](http://en.wikipedia.org/wiki/LM_hash)\n\n\n## License (MIT)\n\nCopyright (c) Sam Decrock <https://github.com/SamDecrock/>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n",
+  "readmeFilename": "README.md",
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/SamDecrock/node-http-ntlm.git"
+  },
+  "scripts": {
+    "jshint": "jshint *.js"
+  },
+  "version": "1.6.1"
+}
diff --git a/node_modules/httpreq/.eslintrc b/node_modules/httpreq/.eslintrc
new file mode 100644
index 0000000..4bc61d6
--- /dev/null
+++ b/node_modules/httpreq/.eslintrc
@@ -0,0 +1,155 @@
+{
+  "ecmaFeatures": {
+    "modules": true
+  },
+
+  "env": {
+    "node": true
+  },
+
+  "rules": {
+    "array-bracket-spacing": [2, "never"],
+    "brace-style": [2, "1tbs", {
+      "allowSingleLine": true
+    }],
+    "camelcase": [2, {
+      "properties": "never"
+    }],
+    "comma-spacing": [2, {
+      "before": false,
+      "after": true
+    }],
+    "comma-style": [2, "last"],
+    "comma-dangle": [2, "never"],
+    "complexity": [1, 8],
+    "computed-property-spacing": [2, "never"],
+    "consistent-return": 1,
+    "curly": [2, "all"],
+    "default-case": 2,
+    "dot-notation": [1, {
+      "allowKeywords": true
+    }],
+    "dot-location": [2, "property"],
+    "eol-last": 2,
+    "eqeqeq": 2,
+    "func-style": 0,
+    "guard-for-in": 0,
+    "handle-callback-err": [2, "^(e|er|err|error)[0-9]{1,2}?$"],
+    "indent": [2, 2, {
+      "SwitchCase": 1
+    }],
+    "keyword-spacing": 2,
+    "key-spacing": [2, {
+      "beforeColon": false,
+      "afterColon": true
+    }],
+    "lines-around-comment": [2, {
+      "beforeBlockComment": true,
+      "afterBlockComment": true,
+      "beforeLineComment": true,
+      "afterLineComment": false,
+      "allowBlockStart": true,
+      "allowBlockEnd": false
+    }],
+    "linebreak-style": [2, "unix"],
+    "max-nested-callbacks": [1, 3],
+    "new-cap": 0,
+    "newline-after-var": [2, "always"],
+    "no-alert": 2,
+    "no-caller": 2,
+    "no-catch-shadow": 2,
+    "no-delete-var": 2,
+    "no-div-regex": 2,
+    "no-duplicate-case": 2,
+    "no-else-return": 2,
+    "no-empty": 2,
+    "no-empty-character-class": 2,
+    "no-eval": 2,
+    "no-extend-native": 2,
+    "no-extra-semi": 2,
+    "no-fallthrough": 2,
+    "no-floating-decimal": 2,
+    "no-func-assign": 2,
+    "no-implied-eval": 2,
+    "no-inline-comments": 1,
+    "no-invalid-regexp": 2,
+    "no-label-var": 2,
+    "no-labels": 2,
+    "no-lone-blocks": 2,
+    "no-lonely-if": 2,
+    "no-mixed-requires": 0,
+    "no-mixed-spaces-and-tabs": 2,
+    "no-multi-spaces": 2,
+    "no-multi-str": 2,
+    "no-multiple-empty-lines": [2, {
+      "max": 2
+    }],
+    "no-native-reassign": 2,
+    "no-nested-ternary": 2,
+    "no-new-func": 2,
+    "no-new-object": 2,
+    "no-new-wrappers": 2,
+    "no-octal-escape": 2,
+    "no-octal": 2,
+    "no-path-concat": 2,
+    "no-param-reassign": 0,
+    "no-process-env": 0,
+    "no-proto": 2,
+    "no-redeclare": 2,
+    "no-reserved-keys": 0,
+    "no-return-assign": [2, "always"],
+    "no-self-compare": 2,
+    "no-sequences": 2,
+    "no-shadow": 2,
+    "no-shadow-restricted-names": 2,
+    "no-spaced-func": 0,
+    "no-sparse-arrays": 1,
+    "no-sync": 1,
+    "no-ternary": 0,
+    "no-throw-literal": 2,
+    "no-trailing-spaces": 2,
+    "no-undef": 2,
+    "no-undef-init": 2,
+    "no-undefined": 1,
+    "no-underscore-dangle": 2,
+    "no-unexpected-multiline": 2,
+    "no-unneeded-ternary": 2,
+    "no-unreachable": 2,
+    "no-unused-vars": 1,
+    "no-use-before-define": 2,
+    "no-useless-concat": 2,
+    "no-warning-comments": 1,
+    "no-with": 2,
+    "no-wrap-func": 0,
+    "object-curly-spacing": [2, "always", {
+      "objectsInObjects": false,
+      "arraysInObjects": false
+    }],
+    "one-var": [2, "never"],
+    "operator-assignment": [2, "always"],
+    "operator-linebreak": [2, "before"],
+    "padded-blocks": [2, "never"],
+    "quote-props": [2, "consistent"],
+    "quotes": [2, "single", "avoid-escape"],
+    "radix": 2,
+    "semi": 2,
+    "semi-spacing": [2, {
+      "before": false,
+      "after": true
+    }],
+    "space-before-blocks": [2, "always"],
+    "space-before-function-paren": [2, "always"],
+    "space-in-parens": [2, "never"],
+    "space-infix-ops": 2,
+    "space-unary-ops": [2, {
+      "words": true,
+      "nonwords": false
+    }],
+    "spaced-comment": [2, "always"],
+    "use-isnan": 2,
+    "valid-typeof": 2,
+    "vars-on-top": 2,
+    "wrap-regex": 0,
+    "yoda": [2, "never"]
+  }
+}
diff --git a/node_modules/httpreq/.npmignore b/node_modules/httpreq/.npmignore
new file mode 100644
index 0000000..f5f493b
--- /dev/null
+++ b/node_modules/httpreq/.npmignore
@@ -0,0 +1,17 @@
+lib-cov
+*.seed
+*.log
+*.csv
+*.dat
+*.out
+*.pid
+*.gz
+
+pids
+logs
+results
+
+node_modules
+
+npm-debug.log
+.DS_Store
\ No newline at end of file
diff --git a/node_modules/httpreq/LICENSE b/node_modules/httpreq/LICENSE
new file mode 100644
index 0000000..2e45053
--- /dev/null
+++ b/node_modules/httpreq/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2017 Sam Decrock <sam.decrock@gmail.com>
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/node_modules/httpreq/README.md b/node_modules/httpreq/README.md
new file mode 100644
index 0000000..c8108f6
--- /dev/null
+++ b/node_modules/httpreq/README.md
@@ -0,0 +1,325 @@
+node-httpreq
+============
+
+node-httpreq is a node.js library to do HTTP(S) requests the easy way
+
+Do GET, POST, PUT, PATCH, DELETE, OPTIONS, upload files, use cookies, change headers, ...
+
+## Install
+
+You can install __httpreq__ using the Node Package Manager (npm):
+
+    npm install httpreq
+
+## Simple example
+```js
+var httpreq = require('httpreq');
+
+httpreq.get('http://www.google.com', function (err, res){
+  if (err) return console.log(err);
+
+  console.log(res.statusCode);
+  console.log(res.headers);
+  console.log(res.body);
+  console.log(res.cookies);
+});
+```
+
+## How to use
+
+* [httpreq.get(url, [options], callback)](#get)
+* [httpreq.post(url, [options], callback)](#post)
+* [httpreq.put(url, [options], callback)](#put)
+* [httpreq.delete(url, [options], callback)](#delete)
+* [httpreq.options(url, [options], callback)](#options)
+* [Uploading files](#upload)
+* [Downloading a binary file](#binary)
+* [Downloading a file directly to disk](#download)
+* [Sending a custom body](#custombody)
+* [Using a http(s) proxy](#proxy)
+* [httpreq.doRequest(options, callback)](#dorequest)
+
+---------------------------------------
+### httpreq.get(url, [options], callback)
+<a name="get"></a>
+
+__Arguments__
+ - url: The url to connect to. Can be http or https.
+ - options: (all are optional) The following options can be passed:
+    - parameters: an object of query parameters
+    - headers: an object of headers
+    - cookies: an array of cookies
+    - auth: a string for basic authentication. For example `username:password`
+    - binary: true/false (default: false), if true, res.body will a buffer containing the binary data
+    - allowRedirects: (default: __true__ , only with httpreq.get() ), if true, redirects will be followed
+    - maxRedirects: (default: __10__ ). For example 1 redirect will allow for one normal request and 1 extra redirected request.
+    - timeout: (default: __none__ ). Adds a timeout to the http(s) request. Should be in milliseconds.
+    - proxy, if you want to pass your request through a http(s) proxy server:
+        - host: eg: "192.168.0.1"
+        - port: eg: 8888
+        - protocol: (default: __'http'__ ) can be 'http' or 'https'
+     - rejectUnauthorized: validate certificate for request with HTTPS. [More here](http://nodejs.org/api/https.html#https_https_request_options_callback)
+ - callback(err, res): A callback function which is called when the request is complete. __res__ contains the headers ( __res.headers__ ), the http status code ( __res.statusCode__ ) and the body ( __res.body__ )
+
+__Example without options__
+
+```js
+var httpreq = require('httpreq');
+
+httpreq.get('http://www.google.com', function (err, res){
+  if (err) return console.log(err);
+
+  console.log(res.statusCode);
+  console.log(res.headers);
+  console.log(res.body);
+});
+```
+
+__Example with options__
+
+```js
+var httpreq = require('httpreq');
+
+httpreq.get('http://posttestserver.com/post.php', {
+  parameters: {
+    name: 'John',
+    lastname: 'Doe'
+  },
+  headers:{
+    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:18.0) Gecko/20100101 Firefox/18.0'
+  },
+  cookies: [
+    'token=DGcGUmplWQSjfqEvmu%2BZA%2Fc',
+    'id=2'
+  ]
+}, function (err, res){
+  if (err){
+    console.log(err);
+  }else{
+    console.log(res.body);
+  }
+});
+```
+---------------------------------------
+### httpreq.post(url, [options], callback)
+<a name="post"></a>
+
+__Arguments__
+ - url: The url to connect to. Can be http or https.
+ - options: (all are optional) The following options can be passed:
+    - parameters: an object of post parameters (content-type is set to *application/x-www-form-urlencoded; charset=UTF-8*)
+    - json: if you want to send json directly (content-type is set to *application/json*)
+    - files: an object of files to upload (content-type is set to *multipart/form-data; boundary=xxx*)
+    - body: custom body content you want to send. If used, previous options will be ignored and your custom body will be sent. (content-type will not be set)
+    - headers: an object of headers
+    - cookies: an array of cookies
+    - auth: a string for basic authentication. For example `username:password`
+    - binary: true/false (default: __false__ ), if true, res.body will be a buffer containing the binary data
+    - allowRedirects: (default: __false__ ), if true, redirects will be followed
+    - maxRedirects: (default: __10__ ). For example 1 redirect will allow for one normal request and 1 extra redirected request.
+    - encodePostParameters: (default: __true__ ), if true, POST/PUT parameters names will be URL encoded.
+    - timeout: (default: none). Adds a timeout to the http(s) request. Should be in milliseconds.
+    - proxy, if you want to pass your request through a http(s) proxy server:
+        - host: eg: "192.168.0.1"
+        - port: eg: 8888
+        - protocol: (default: __'http'__ ) can be 'http' or 'https'
+    - rejectUnauthorized: validate certificate for request with HTTPS. [More here](http://nodejs.org/api/https.html#https_https_request_options_callback)
+ - callback(err, res): A callback function which is called when the request is complete. __res__ contains the headers ( __res.headers__ ), the http status code ( __res.statusCode__ ) and the body ( __res.body__ )
+
+__Example without extra options__
+
+```js
+var httpreq = require('httpreq');
+
+httpreq.post('http://posttestserver.com/post.php', {
+  parameters: {
+    name: 'John',
+    lastname: 'Doe'
+  }
+}, function (err, res){
+  if (err){
+    console.log(err);
+  }else{
+    console.log(res.body);
+  }
+});
+```
+
+__Example with options__
+
+```js
+var httpreq = require('httpreq');
+
+httpreq.post('http://posttestserver.com/post.php', {
+  parameters: {
+    name: 'John',
+    lastname: 'Doe'
+  },
+  headers:{
+    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:18.0) Gecko/20100101 Firefox/18.0'
+  },
+  cookies: [
+    'token=DGcGUmplWQSjfqEvmu%2BZA%2Fc',
+    'id=2'
+  ]
+}, function (err, res){
+  if (err){
+    console.log(err);
+  }else{
+    console.log(res.body);
+  }
+});
+```
+
+---------------------------------------
+### httpreq.put(url, [options], callback)
+<a name="put"></a>
+
+Same options as [httpreq.post(url, [options], callback)](#post)
+
+---------------------------------------
+<a name="delete" />
+### httpreq.delete(url, [options], callback)
+
+Same options as [httpreq.post(url, [options], callback)](#post)
+
+---------------------------------------
+<a name="options" />
+### httpreq.options(url, [options], callback)
+
+Same options as [httpreq.get(url, [options], callback)](#get) except for the ability to follow redirects.
+
+---------------------------------------
+<a name="upload" />
+### Uploading files
+
+You can still use ```httpreq.uploadFiles({url: 'url', files: {}}, callback)```, but it's easier to just use POST (or PUT):
+
+__Example__
+
+```js
+var httpreq = require('httpreq');
+
+httpreq.post('http://posttestserver.com/upload.php', {
+  parameters: {
+    name: 'John',
+    lastname: 'Doe'
+  },
+  files:{
+    myfile: __dirname + "/testupload.jpg",
+    myotherfile: __dirname + "/testupload.jpg"
+  }
+}, function (err, res){
+  if (err) throw err;
+});
+```
+
+---------------------------------------
+<a name="binary"></a>
+### Downloading a binary file
+To download a binary file, just add __binary: true__ to the options when doing a get or a post.
+
+__Example__
+
+```js
+var httpreq = require('httpreq');
+
+httpreq.get('https://ssl.gstatic.com/gb/images/k1_a31af7ac.png', {binary: true}, function (err, res){
+  if (err){
+    console.log(err);
+  }else{
+    fs.writeFile(__dirname + '/test.png', res.body, function (err) {
+      if(err)
+        console.log("error writing file");
+    });
+  }
+});
+```
+
+---------------------------------------
+<a name="download"></a>
+### Downloading a file directly to disk
+To download a file directly to disk, use the download method provided.
+
+Downloading is done using a stream, so the data is not stored in memory and directly saved to file.
+
+__Example__
+
+```js
+var httpreq = require('httpreq');
+
+httpreq.download(
+  'https://ssl.gstatic.com/gb/images/k1_a31af7ac.png',
+  __dirname + '/test.png'
+, function (err, progress){
+  if (err) return console.log(err);
+  console.log(progress);
+}, function (err, res){
+  if (err) return console.log(err);
+  console.log(res);
+});
+
+```
+---------------------------------------
+<a name="custombody"></a>
+### Sending a custom body
+Use the body option to send a custom body (eg. an xml post)
+
+__Example__
+
+```js
+var httpreq = require('httpreq');
+
+httpreq.post('http://posttestserver.com/post.php',{
+  body: '<?xml version="1.0" encoding="UTF-8"?>',
+  headers:{
+    'Content-Type': 'text/xml',
+  }},
+  function (err, res) {
+    if (err){
+      console.log(err);
+    }else{
+      console.log(res.body);
+    }
+  }
+);
+```
+
+---------------------------------------
+<a name="proxy"></a>
+### Using a http(s) proxy
+
+__Example__
+
+```js
+var httpreq = require('httpreq');
+
+httpreq.post('http://posttestserver.com/post.php', {
+  proxy: {
+    host: '10.100.0.126',
+    port: 8888
+  }
+}, function (err, res){
+  if (err){
+    console.log(err);
+  }else{
+    console.log(res.body);
+  }
+});
+```
+
+---------------------------------------
+### httpreq.doRequest(options, callback)
+<a name="dorequest"></a>
+
+httpreq.doRequest is internally used by httpreq.get() and httpreq.post(). You can use this directly. Everything is stays the same as httpreq.get() or httpreq.post() except that the following options MUST be passed:
+- url: the url to post the files to
+- method: 'GET', 'POST', 'PUT' or 'DELETE'
+
+## Donate
+
+If you like this module or you want me to update it faster, feel free to donate. It helps increasing my dedication to fixing bugs :-)
+
+[![](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=AB3R2SUL53K7S)
+
+
diff --git a/node_modules/httpreq/contributors.md b/node_modules/httpreq/contributors.md
new file mode 100644
index 0000000..5b2775b
--- /dev/null
+++ b/node_modules/httpreq/contributors.md
@@ -0,0 +1,26 @@
+###### Contributors
+[Sam](https://github.com/SamDecrock)
+<font color="#999">63 Commits</font> / <font color="#6cc644">2309++</font> / <font color="#bd3c00"> 1040--</font>
+<font color="#dedede">81.82%&nbsp;<font color="#dedede">||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||</font><font color="#f4f4f4">||||||||||||||||||||||||||||||||</font><br><br>
+[Franklin van de Meent](https://github.com/fvdm)
+<font color="#999">8 Commits</font> / <font color="#6cc644">51++</font> / <font color="#bd3c00"> 16--</font>
+<font color="#dedede">10.39%&nbsp;<font color="#dedede">||||||||||||||||||</font><font color="#f4f4f4">||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||</font><br><br>
+[Russell Beattie](https://github.com/russellbeattie)
+<font color="#999">1 Commits</font> / <font color="#6cc644">55++</font> / <font color="#bd3c00"> 3--</font>
+<font color="#dedede">01.30%&nbsp;<font color="#dedede">||</font><font color="#f4f4f4">||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||</font><br><br>
+[Jason Prickett MSFT](https://github.com/jpricketMSFT)
+<font color="#999">1 Commits</font> / <font color="#6cc644">5++</font> / <font color="#bd3c00"> 0--</font>
+<font color="#dedede">01.30%&nbsp;<font color="#dedede">||</font><font color="#f4f4f4">||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||</font><br><br>
+[null](https://github.com/jjharriso)
+<font color="#999">1 Commits</font> / <font color="#6cc644">12++</font> / <font color="#bd3c00"> 0--</font>
+<font color="#dedede">01.30%&nbsp;<font color="#dedede">||</font><font color="#f4f4f4">||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||</font><br><br>
+[MJJ](https://github.com/mjj2000)
+<font color="#999">1 Commits</font> / <font color="#6cc644">11++</font> / <font color="#bd3c00"> 1--</font>
+<font color="#dedede">01.30%&nbsp;<font color="#dedede">||</font><font color="#f4f4f4">||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||</font><br><br>
+[Jeff Young](https://github.com/jeffyoung)
+<font color="#999">1 Commits</font> / <font color="#6cc644">19++</font> / <font color="#bd3c00"> 1--</font>
+<font color="#dedede">01.30%&nbsp;<font color="#dedede">||</font><font color="#f4f4f4">||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||</font><br><br>
+[Dave Preston](https://github.com/davepreston)
+<font color="#999">1 Commits</font> / <font color="#6cc644">5++</font> / <font color="#bd3c00"> 0--</font>
+<font color="#dedede">01.30%&nbsp;<font color="#dedede">||</font><font color="#f4f4f4">||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||</font><br><br>
+###### [Generated](https://github.com/jakeleboeuf/contributor) on Mon May 02 2016 11:08:45 GMT+0200 (CEST)
\ No newline at end of file
diff --git a/node_modules/httpreq/examples.js b/node_modules/httpreq/examples.js
new file mode 100644
index 0000000..5ee1f08
--- /dev/null
+++ b/node_modules/httpreq/examples.js
@@ -0,0 +1,214 @@
+var httpreq = require('./lib/httpreq');
+fs = require('fs')
+
+
+// example1(); // get www.google.com
+// example2(); // do some post
+// example3(); // same as above + extra headers + cookies
+// example4(); // https also works:
+// example5(); // uploading some file:
+// example6(); // u can use doRequest instead of .get or .post
+// example7(); // download a binary file:
+// example8(); // send json
+// example9(); // send your own body content (eg. xml)
+// example10(); // set max redirects:
+// example11(); // set timeout
+// example12(); // // download file directly to disk
+
+
+// get www.google.com
+function example1(){
+	httpreq.get('http://www.google.com', function (err, res){
+		if (err){
+			console.log(err);
+		}else{
+			console.log(res.headers); //headers are stored in res.headers
+			console.log(res.body); //content of the body is stored in res.body
+		}
+	});
+}
+
+// do some post
+function example2(){
+	httpreq.post('http://posttestserver.com/post.php', {
+		parameters: {
+			name: 'John',
+			lastname: 'Doe'
+		}
+	}, function (err, res){
+		if (err){
+			console.log(err);
+		}else{
+			console.log(res.body);
+		}
+	});
+}
+
+// same as above + extra headers + cookies
+function example3(){
+	httpreq.post('http://posttestserver.com/post.php', {
+		parameters: {
+			name: 'John',
+			lastname: 'Doe'
+		},
+		headers:{
+			'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:18.0) Gecko/20100101 Firefox/18.0'
+		},
+		cookies: [
+			'token=DGcGUmplWQSjfqEvmu%2BZA%2Fc',
+			'id=2'
+		]
+	}, function (err, res){
+		if (err){
+			console.log(err);
+		}else{
+			console.log(res.body);
+		}
+	});
+}
+
+// https also works:
+function example4(){
+	httpreq.get('https://graph.facebook.com/19292868552', function (err, res){
+		if (err){
+			console.log(err);
+		}else{
+			console.log(JSON.parse(res.body));
+		}
+	});
+}
+
+// uploading some file:
+function example5(){
+	httpreq.uploadFiles({
+		url: "http://rekognition.com/demo/do_upload/",
+		parameters:{
+			name_space	: 'something',
+		},
+		files:{
+			fileToUpload: __dirname + "/test/testupload.jpg"
+		}},
+	function (err, res){
+		if (err) return console.log(err);
+		console.log(res.body);
+	});
+}
+
+// u can use doRequest instead of .get or .post
+function example6(){
+	httpreq.doRequest({
+		url: 'https://graph.facebook.com/19292868552',
+		method: 'GET',
+		parameters: {
+			name: 'test'
+		}
+	},
+	function (err, res){
+		if (err){
+			console.log(err);
+		}else{
+			console.log(JSON.parse(res.body));
+		}
+	});
+}
+
+// download a binary file:
+function example7(){
+	httpreq.get('https://ssl.gstatic.com/gb/images/k1_a31af7ac.png', {
+		binary: true,
+		progressCallback: function (err, progress) {
+			console.log(progress);
+		}
+	},
+	function (err, res){
+		if (err){
+			console.log(err);
+		}else{
+			fs.writeFile(__dirname + '/test.png', res.body, function (err) {
+				if(err) return console.log("error writing file");
+			});
+		}
+	});
+}
+
+// send json
+function example8(){
+	httpreq.post('http://posttestserver.com/post.php',{
+		json: {name: 'John', lastname: 'Do'},
+		headers:{
+			'Content-Type': 'text/xml',
+		}},
+		function (err, res) {
+			if (err){
+				console.log(err);
+			}else{
+				console.log(res.body);
+			}
+		}
+	);
+}
+
+// send your own body content (eg. xml):
+function example9(){
+	httpreq.post('http://posttestserver.com/post.php',{
+		body: '<?xml version="1.0" encoding="UTF-8"?>',
+		headers:{
+			'Content-Type': 'text/xml',
+		}},
+		function (err, res) {
+			if (err){
+				console.log(err);
+			}else{
+				console.log(res.body);
+			}
+		}
+	);
+}
+
+// set max redirects:
+function example10(){
+	httpreq.get('http://scobleizer.com/feed/',{
+		maxRedirects: 2, // default is 10
+		headers:{
+			'User-Agent': 'Magnet', //for some reason causes endless redirects on this site...
+		}},
+		function (err, res) {
+			if (err){
+				console.log(err);
+			}else{
+				console.log(res.body);
+			}
+		}
+	);
+}
+
+// set timeout
+function example11(){
+	httpreq.get('http://localhost:3000/',{
+		timeout: (5 * 1000) // timeout in milliseconds
+		},
+		function (err, res) {
+			if (err){
+				console.log(err);
+			}else{
+				console.log(res.body);
+			}
+		}
+	);
+}
+
+// download file directly to disk:
+function example12 () {
+	httpreq.download(
+		'https://ssl.gstatic.com/gb/images/k1_a31af7ac.png',
+		__dirname + '/test.png'
+	, function (err, progress){
+		if (err) return console.log(err);
+		console.log(progress);
+	}, function (err, res){
+		if (err) return console.log(err);
+		console.log(res);
+	});
+}
+
+
diff --git a/node_modules/httpreq/lib/httpreq.js b/node_modules/httpreq/lib/httpreq.js
new file mode 100644
index 0000000..ad58609
--- /dev/null
+++ b/node_modules/httpreq/lib/httpreq.js
@@ -0,0 +1,621 @@
+/*
+Copyright (c) 2013 Sam Decrock <sam.decrock@gmail.com>
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+var querystring = require ('querystring');
+var https = require ('https');
+var http = require ('http');
+var url = require ('url');
+var fs = require ('fs');
+
+
+/**
+ * Generate multipart boundary
+ *
+ * @returns {string}
+ */
+
+function generateBoundary () {
+  var boundary = '---------------------------';
+  var charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+
+  for (var i = 0; i < 29; i++) {
+    boundary += charset.charAt (Math.floor (Math.random () * charset.length));
+  }
+
+  return boundary;
+}
+
+
+/**
+ * Extract cookies from headers
+ *
+ * @param headers {object} - Response headers
+ * @returns {array} - Extracted cookie strings
+ */
+
+function extractCookies (headers) {
+  var rawcookies = headers['set-cookie'];
+
+  if (!rawcookies) {
+    return [];
+  }
+
+  if (rawcookies == []) {
+    return [];
+  }
+
+  var cookies = [];
+  for (var i = 0; i < rawcookies.length; i++) {
+    var rawcookie = rawcookies[i].split (';');
+    if (rawcookie[0]) {
+      cookies.push (rawcookie[0]);
+    }
+  }
+  return cookies;
+}
+
+
+/**
+ * Custom HTTP request
+ *
+ * @callback callback
+ * @param o {object} - Request options
+ * @param callback [function] - Process response
+ * @returns {void}
+ */
+
+function doRequest (o, callback) {
+  if (!callback) {
+    callback = function (err) {}; // dummy function
+  }
+
+  // prevent multiple callbacks
+  var finalCallbackDone = false;
+  function finalCallback (err, res) {
+    if (!finalCallbackDone) {
+      finalCallbackDone = true;
+      callback (err, res);
+    }
+  }
+
+  if (o.maxRedirects === undefined) {
+    o.maxRedirects = 10;
+  }
+
+  if (o.encodePostParameters === undefined) {
+    o.encodePostParameters = true;
+  }
+
+  var chunks = [];
+  var body; // Buffer
+  var contentType;
+
+  var port;
+  var host;
+  var path;
+  var isHttps = false;
+
+  if (o.proxy) {
+    port = o.proxy.port;
+    host = o.proxy.host;
+    path = o.url; // complete url
+
+    if (o.proxy.protocol && o.proxy.protocol.match (/https/)) {
+      isHttps = true;
+    }
+  } else {
+    var reqUrl = url.parse (o.url);
+    host = reqUrl.hostname;
+    path = reqUrl.path;
+
+    if (reqUrl.protocol === 'https:') {
+      isHttps = true;
+    }
+
+    if (reqUrl.port) {
+      port = reqUrl.port;
+    } else if (isHttps) {
+      port = 443;
+    } else {
+      port = 80;
+    }
+  }
+
+  if (o.files && o.files.length > 0 && o.method === 'GET') {
+    var err = new Error ('Can\'t send files using GET');
+    err.code = 'CANT_SEND_FILES_USING_GET';
+    return finalCallback (err);
+  }
+
+  if (o.parameters) {
+    if (o.method === 'GET') {
+      path += '?' + querystring.stringify (o.parameters);
+    } else {
+      body = new Buffer (querystring.stringify (o.parameters), 'utf8');
+      contentType = 'application/x-www-form-urlencoded; charset=UTF-8';
+    }
+  }
+
+  if (o.json) {
+    body = new Buffer (JSON.stringify (o.json), 'utf8');
+    contentType = 'application/json';
+  }
+
+  if (o.files) {
+    var crlf = '\r\n';
+    var boundary = generateBoundary ();
+    var separator = '--' + boundary;
+    var bodyArray = new Array (); // temporary body array
+
+    // if the user wants to POST/PUT files, other parameters need to be encoded using 'Content-Disposition'
+    for (var key in o.parameters) {
+      // According to RFC 2388 (https://www.ietf.org/rfc/rfc2388.txt)
+      // "Field names originally in non-ASCII character sets MAY be encoded
+        // within the value of the "name" parameter using the standard method
+        // described in RFC 2047."
+        // -- encodePostParameters -- true by default and MAY be changed by the user
+      var headerKey = o.encodePostParameters ? encodeURIComponent (key) : key;
+      var encodedParameter = separator + crlf
+        + 'Content-Disposition: form-data; name="' + headerKey + '"' + crlf
+        + crlf
+        + o.parameters[key] + crlf;
+      bodyArray.push (new Buffer (encodedParameter));
+    }
+
+    // now for the files:
+    var haveAlreadyAddedAFile = false;
+
+    for (var file in o.files) {
+      var filepath = o.files[file];
+      var filename = filepath.replace (/\\/g, '/').replace (/.*\//, '');
+
+      var encodedFile = separator + crlf
+        + 'Content-Disposition: form-data; name="' + file + '"; filename="' + filename + '"' + crlf
+        + 'Content-Type: application/octet-stream' + crlf
+        + crlf;
+
+      // add crlf before separator if we have already added a file
+      if (haveAlreadyAddedAFile) {
+        encodedFile = crlf + encodedFile;
+      }
+
+      bodyArray.push (new Buffer (encodedFile));
+
+      // add binary file:
+      bodyArray.push (require ('fs').readFileSync (filepath));
+
+      haveAlreadyAddedAFile = true;
+    }
+
+    var footer = crlf + separator + '--' + crlf;
+    bodyArray.push (new Buffer (footer));
+
+    // set body and contentType:
+    body = Buffer.concat (bodyArray);
+    contentType = 'multipart/form-data; boundary=' + boundary;
+  }
+
+  // overwrites the body if the user passes a body:
+  // clears the content-type
+  if (o.body) {
+    body = new Buffer (o.body, 'utf8');
+    contentType = null;
+  }
+
+
+  var requestoptions = {
+    host: host,
+    port: port,
+    path: path,
+    method: o.method,
+    headers: {}
+  };
+
+  if (!o.redirectCount) {
+    o.redirectCount = 0;
+  }
+
+  if (body) {
+    requestoptions.headers['Content-Length'] = body.length;
+  }
+
+  if (contentType) {
+    requestoptions.headers['Content-Type'] = contentType;
+  }
+
+  if (o.cookies) {
+    requestoptions.headers.Cookie = o.cookies.join ('; ');
+  }
+
+  if (o.rejectUnauthorized !== undefined && isHttps) {
+    requestoptions.rejectUnauthorized = o.rejectUnauthorized;
+  }
+
+  if (isHttps && o.key) {
+    requestoptions.key = o.key;
+  }
+
+  if (isHttps && o.cert) {
+    requestoptions.cert = o.cert;
+  }
+
+  if (isHttps && o.secureProtocol) {
+    requestoptions.secureProtocol = o.secureProtocol;
+  }
+
+  if (isHttps && o.ciphers) {
+    requestoptions.ciphers = o.ciphers;
+  }
+
+  if (isHttps && o.passphrase) {
+    requestoptions.passphrase = o.passphrase;
+  }
+
+  if (isHttps && o.pfx) {
+    requestoptions.pfx = o.pfx;
+  }
+
+  if (isHttps && o.ca) {
+    requestoptions.ca = o.ca;
+  }
+
+  // add custom headers:
+  if (o.headers) {
+    for (var headerkey in o.headers) {
+      requestoptions.headers[headerkey] = o.headers[headerkey];
+    }
+  }
+
+  if (o.agent) {
+    requestoptions.agent = o.agent;
+  }
+
+  if (o.auth) {
+    requestoptions.auth = o.auth;
+  }
+
+  if (o.localAddress) {
+    requestoptions.localAddress = o.localAddress;
+  }
+
+  if (o.secureOptions) {
+    requestoptions.secureOptions = o.secureOptions;
+  }
+
+
+  /**
+   * Process request response
+   *
+   * @param res {object} - Response details
+   * @returns {void}
+   */
+
+  function requestResponse (res) {
+    var ended = false;
+    var currentsize = 0;
+
+    var downloadstream = null;
+    if (o.downloadlocation) {
+      downloadstream = fs.createWriteStream (o.downloadlocation);
+    }
+
+    res.on ('data', function (chunk) {
+      if (o.downloadlocation) {
+        downloadstream.write (chunk); //write it to disk, not to memory
+      } else {
+        chunks.push (chunk);
+      }
+
+      if (o.progressCallback) {
+        var totalsize = res.headers['content-length'];
+        if (totalsize) {
+          currentsize += chunk.length;
+
+          o.progressCallback (null, {
+            url: o.url,
+            totalsize: totalsize,
+            currentsize: currentsize,
+            percentage: currentsize * 100 / totalsize
+          });
+        } else {
+          o.progressCallback (new Error ('no content-length specified for file, so no progress monitoring possible'));
+        }
+      }
+    });
+
+    res.on ('end', function (err) {
+      ended = true;
+
+      // check for redirects
+      if (res.headers.location && o.allowRedirects) {
+        // Close any open file
+        if (o.downloadlocation) {
+          downloadstream.end ();
+        }
+
+        if (o.redirectCount < o.maxRedirects) {
+          o.redirectCount++;
+          o.url = res.headers.location;
+          o.cookies = extractCookies (res.headers);
+          return doRequest (o, finalCallback);
+        } else {
+          var err = new Error ('Too many redirects (> ' + o.maxRedirects + ')');
+          err.code = 'TOOMANYREDIRECTS';
+          err.redirects = o.maxRedirects;
+          return finalCallback (err);
+        }
+      }
+
+      if (!o.downloadlocation) {
+        var responsebody = Buffer.concat (chunks);
+        if (!o.binary) {
+          responsebody = responsebody.toString ('utf8');
+        }
+
+        finalCallback (null, {
+          headers: res.headers,
+          statusCode: res.statusCode,
+          body: responsebody,
+          cookies: extractCookies (res.headers)
+        });
+      } else {
+        downloadstream.end (null, null, function () {
+          finalCallback (null, {
+            headers: res.headers,
+            statusCode: res.statusCode,
+            downloadlocation: o.downloadlocation,
+            cookies: extractCookies (res.headers)
+          });
+        });
+      }
+    });
+
+    res.on ('close', function () {
+      if (!ended) {
+        finalCallback (new Error ('Request aborted'));
+      }
+    });
+  }
+
+  var request;
+
+  // remove headers with undefined keys or values
+  // else we get an error in Node 0.12.0 about "setHeader ()"
+  for (var headerName in requestoptions.headers) {
+    var headerValue = requestoptions.headers[headerName];
+    if (!headerName || !headerValue) {
+      delete requestoptions.headers[headerName];
+    }
+  }
+
+  if (isHttps) {
+    request = https.request (requestoptions, requestResponse);
+  } else {
+    request = http.request (requestoptions, requestResponse);
+  }
+
+  if (o.timeout) {
+    request.setTimeout (parseInt (o.timeout, 10), function () {
+      var err = new Error ('request timed out');
+      err.code = 'TIMEOUT';
+      finalCallback (err);
+      request.abort ();
+    });
+  }
+
+  request.on ('error', function (err) {
+    finalCallback (err);
+  });
+
+  if (body) {
+    request.write (body);
+  }
+
+  request.end ();
+}
+
+exports.doRequest = doRequest;
+
+
+/**
+ * HTTP GET method
+ *
+ * @callback callback
+ * @param url {string} - Request URL
+ * @param [options] {object} - Request options
+ * @param callback [function] - Process response
+ * @returns {void}
+ */
+
+exports.get = function (url, options, callback) {
+  if (callback === undefined && options && typeof options === 'function') {
+    callback = options;
+  }
+
+  var moreOptions = options;
+  moreOptions.url = url;
+  moreOptions.method = 'GET';
+
+  if (moreOptions.allowRedirects === undefined) {
+    moreOptions.allowRedirects = true;
+  }
+
+  doRequest (moreOptions, callback);
+};
+
+
+/**
+ * HTTP OPTIONS method
+ *
+ * @callback callback
+ * @param url {string} - Request URL
+ * @param [options] {object} - Request options
+ * @param callback [function] - Process response
+ * @returns {void}
+ */
+
+exports.options = function (url, options, callback) {
+  if (callback === undefined && options && typeof options === 'function') {
+    callback = options;
+  }
+
+  var moreOptions = options;
+  moreOptions.url = url;
+  moreOptions.method = 'OPTIONS';
+  doRequest (moreOptions, callback);
+};
+
+
+/**
+ * HTTP POST method
+ *
+ * @callback callback
+ * @param url {string} - Request URL
+ * @param [options] {object} - Request options
+ * @param callback [function] - Process response
+ * @returns {void}
+ */
+
+exports.post = function (url, options, callback) {
+  if (callback === undefined && options && typeof options === 'function') {
+    callback = options;
+  }
+
+  var moreOptions = options;
+  moreOptions.url = url;
+  moreOptions.method = 'POST';
+  doRequest (moreOptions, callback);
+};
+
+
+/**
+ * HTTP PUT method
+ *
+ * @callback callback
+ * @param url {string} - Request URL
+ * @param [options] {object} - Request options
+ * @param callback [function] - Process response
+ * @returns {void}
+ */
+
+exports.put = function (url, options, callback) {
+  if (callback === undefined && options && typeof options === 'function') {
+    callback = options;
+  }
+
+  var moreOptions = options;
+  moreOptions.url = url;
+  moreOptions.method = 'PUT';
+  doRequest (moreOptions, callback);
+};
+
+
+/**
+ * HTTP PATCH method
+ *
+ * @callback callback
+ * @param url {string} - Request URL
+ * @param [options] {object} - Request options
+ * @param callback [function] - Process response
+ * @returns {void}
+ */
+
+exports.patch = function (url, options, callback) {
+  if (callback === undefined && options && typeof options === 'function') {
+    callback = options;
+  }
+
+  var moreOptions = options;
+  moreOptions.url = url;
+  moreOptions.method = 'PATCH';
+  doRequest (moreOptions, callback);
+};
+
+
+/**
+ * HTTP DELETE method
+ *
+ * @callback callback
+ * @param url {string} - Request URL
+ * @param [options] {object} - Request options
+ * @param callback [function] - Process response
+ * @returns {void}
+ */
+
+exports.delete = function (url, options, callback) {
+  if (callback === undefined && options && typeof options === 'function') {
+    callback = options;
+  }
+
+  var moreOptions = options;
+  moreOptions.url = url;
+  moreOptions.method = 'DELETE';
+  doRequest (moreOptions, callback);
+};
+
+
+/**
+ * Download a file
+ *
+ * @callback callback
+ * @param url {string} - Request URL
+ * @param downloadlocation {string} - Path where to store file
+ * @param [progressCallback] {function} - Called multiple times during download
+ * @param callback {function} - Called once when download ends
+ * @returns {void}
+ */
+
+exports.download = function (url, downloadlocation, progressCallback, callback) {
+  var options = {};
+  options.url = url;
+  options.method = 'GET';
+  options.downloadlocation = downloadlocation;
+  options.allowRedirects = true;
+
+  // if only 3 args are provided, so no progressCallback
+  if (callback === undefined && progressCallback && typeof progressCallback === 'function') {
+    callback = progressCallback;
+  } else {
+    options.progressCallback = progressCallback;
+  }
+
+  doRequest (options, callback);
+};
+
+
+/**
+ * Upload files
+ * old function, can still be used
+ *
+ * @callback callback
+ * @param options {object} - Request options
+ * @param callback [function] - Process response
+ * @returns {void}
+ */
+
+exports.uploadFiles = function (options, callback) {
+  var moreOptions = options;
+  moreOptions.method = 'POST';
+  doRequest (moreOptions, callback);
+};
diff --git a/node_modules/httpreq/package.json b/node_modules/httpreq/package.json
new file mode 100644
index 0000000..d1be53d
--- /dev/null
+++ b/node_modules/httpreq/package.json
@@ -0,0 +1,125 @@
+{
+  "_args": [
+    [
+      {
+        "raw": "httpreq@>=0.4.22",
+        "scope": null,
+        "escapedName": "httpreq",
+        "name": "httpreq",
+        "rawSpec": ">=0.4.22",
+        "spec": ">=0.4.22",
+        "type": "range"
+      },
+      "/Users/fzy/project/koa2_Sequelize_project/node_modules/httpntlm"
+    ]
+  ],
+  "_from": "httpreq@>=0.4.22",
+  "_id": "httpreq@0.4.24",
+  "_inCache": true,
+  "_location": "/httpreq",
+  "_nodeVersion": "6.9.1",
+  "_npmOperationalInternal": {
+    "host": "s3://npm-registry-packages",
+    "tmp": "tmp/httpreq-0.4.24.tgz_1498854530181_0.7929337220266461"
+  },
+  "_npmUser": {
+    "name": "samdecrock",
+    "email": "sam.decrock@gmail.com"
+  },
+  "_npmVersion": "3.10.8",
+  "_phantomChildren": {},
+  "_requested": {
+    "raw": "httpreq@>=0.4.22",
+    "scope": null,
+    "escapedName": "httpreq",
+    "name": "httpreq",
+    "rawSpec": ">=0.4.22",
+    "spec": ">=0.4.22",
+    "type": "range"
+  },
+  "_requiredBy": [
+    "/httpntlm"
+  ],
+  "_resolved": "https://registry.npmjs.org/httpreq/-/httpreq-0.4.24.tgz",
+  "_shasum": "4335ffd82cd969668a39465c929ac61d6393627f",
+  "_shrinkwrap": null,
+  "_spec": "httpreq@>=0.4.22",
+  "_where": "/Users/fzy/project/koa2_Sequelize_project/node_modules/httpntlm",
+  "author": {
+    "name": "Sam Decrock",
+    "url": "https://github.com/SamDecrock/"
+  },
+  "bugs": {
+    "url": "https://github.com/SamDecrock/node-httpreq/issues"
+  },
+  "contributors": [
+    {
+      "name": "Russell Beattie",
+      "email": "russ@russellbeattie.com",
+      "url": "https://github.com/russellbeattie"
+    },
+    {
+      "name": "Jason Prickett MSFT",
+      "url": "https://github.com/jpricketMSFT"
+    },
+    {
+      "url": "https://github.com/jjharriso"
+    },
+    {
+      "name": "Sam",
+      "url": "https://github.com/SamDecrock"
+    },
+    {
+      "name": "MJJ",
+      "url": "https://github.com/mjj2000"
+    },
+    {
+      "name": "Jeff Young",
+      "url": "https://github.com/jeffyoung"
+    },
+    {
+      "name": "Dave Preston",
+      "url": "https://github.com/davepreston"
+    },
+    {
+      "name": "Franklin van de Meent",
+      "email": "fr@nkl.in",
+      "url": "https://github.com/fvdm"
+    }
+  ],
+  "dependencies": {},
+  "description": "node-httpreq is a node.js library to do HTTP(S) requests the easy way",
+  "devDependencies": {
+    "chai": "~1.9.1",
+    "express": "3.0.3",
+    "mocha": "~1.20.1"
+  },
+  "directories": {},
+  "dist": {
+    "shasum": "4335ffd82cd969668a39465c929ac61d6393627f",
+    "tarball": "https://registry.npmjs.org/httpreq/-/httpreq-0.4.24.tgz"
+  },
+  "engines": {
+    "node": ">= 0.8.0"
+  },
+  "gitHead": "a48045e87f378079f4e83ed18d6032292cb4a854",
+  "homepage": "https://github.com/SamDecrock/node-httpreq#readme",
+  "license": "MIT",
+  "main": "./lib/httpreq",
+  "maintainers": [
+    {
+      "name": "samdecrock",
+      "email": "sam.decrock@gmail.com"
+    }
+  ],
+  "name": "httpreq",
+  "optionalDependencies": {},
+  "readme": "node-httpreq\n============\n\nnode-httpreq is a node.js library to do HTTP(S) requests the easy way\n\nDo GET, POST, PUT, PATCH, DELETE, OPTIONS, upload files, use cookies, change headers, ...\n\n## Install\n\nYou can install __httpreq__ using the Node Package Manager (npm):\n\n    npm install httpreq\n\n## Simple example\n```js\nvar httpreq = require('httpreq');\n\nhttpreq.get('http://www.google.com', function (err, res){\n  if (err) return console.log(err);\n\n  console.log(res.statusCode);\n  console.log(res.headers);\n  console.log(res.body);\n  console.log(res.cookies);\n});\n```\n\n## How to use\n\n* [httpreq.get(url, [options], callback)](#get)\n* [httpreq.post(url, [options], callback)](#post)\n* [httpreq.put(url, [options], callback)](#put)\n* [httpreq.delete(url, [options], callback)](#delete)\n* [httpreq.options(url, [options], callback)](#options)\n* [Uploading files](#upload)\n* [Downloading a binary file](#binary)\n* [Downloading a file directly to disk](#download)\n* [Sending a custom body](#custombody)\n* [Using a http(s) proxy](#proxy)\n* [httpreq.doRequest(options, callback)](#dorequest)\n\n---------------------------------------\n### httpreq.get(url, [options], callback)\n<a name=\"get\"></a>\n\n__Arguments__\n - url: The url to connect to. Can be http or https.\n - options: (all are optional) The following options can be passed:\n    - parameters: an object of query parameters\n    - headers: an object of headers\n    - cookies: an array of cookies\n    - auth: a string for basic authentication. For example `username:password`\n    - binary: true/false (default: false), if true, res.body will a buffer containing the binary data\n    - allowRedirects: (default: __true__ , only with httpreq.get() ), if true, redirects will be followed\n    - maxRedirects: (default: __10__ ). For example 1 redirect will allow for one normal request and 1 extra redirected request.\n    - timeout: (default: __none__ ). Adds a timeout to the http(s) request. Should be in milliseconds.\n    - proxy, if you want to pass your request through a http(s) proxy server:\n        - host: eg: \"192.168.0.1\"\n        - port: eg: 8888\n        - protocol: (default: __'http'__ ) can be 'http' or 'https'\n     - rejectUnauthorized: validate certificate for request with HTTPS. [More here](http://nodejs.org/api/https.html#https_https_request_options_callback)\n - callback(err, res): A callback function which is called when the request is complete. __res__ contains the headers ( __res.headers__ ), the http status code ( __res.statusCode__ ) and the body ( __res.body__ )\n\n__Example without options__\n\n```js\nvar httpreq = require('httpreq');\n\nhttpreq.get('http://www.google.com', function (err, res){\n  if (err) return console.log(err);\n\n  console.log(res.statusCode);\n  console.log(res.headers);\n  console.log(res.body);\n});\n```\n\n__Example with options__\n\n```js\nvar httpreq = require('httpreq');\n\nhttpreq.get('http://posttestserver.com/post.php', {\n  parameters: {\n    name: 'John',\n    lastname: 'Doe'\n  },\n  headers:{\n    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:18.0) Gecko/20100101 Firefox/18.0'\n  },\n  cookies: [\n    'token=DGcGUmplWQSjfqEvmu%2BZA%2Fc',\n    'id=2'\n  ]\n}, function (err, res){\n  if (err){\n    console.log(err);\n  }else{\n    console.log(res.body);\n  }\n});\n```\n---------------------------------------\n### httpreq.post(url, [options], callback)\n<a name=\"post\"></a>\n\n__Arguments__\n - url: The url to connect to. Can be http or https.\n - options: (all are optional) The following options can be passed:\n    - parameters: an object of post parameters (content-type is set to *application/x-www-form-urlencoded; charset=UTF-8*)\n    - json: if you want to send json directly (content-type is set to *application/json*)\n    - files: an object of files to upload (content-type is set to *multipart/form-data; boundary=xxx*)\n    - body: custom body content you want to send. If used, previous options will be ignored and your custom body will be sent. (content-type will not be set)\n    - headers: an object of headers\n    - cookies: an array of cookies\n    - auth: a string for basic authentication. For example `username:password`\n    - binary: true/false (default: __false__ ), if true, res.body will be a buffer containing the binary data\n    - allowRedirects: (default: __false__ ), if true, redirects will be followed\n    - maxRedirects: (default: __10__ ). For example 1 redirect will allow for one normal request and 1 extra redirected request.\n    - encodePostParameters: (default: __true__ ), if true, POST/PUT parameters names will be URL encoded.\n    - timeout: (default: none). Adds a timeout to the http(s) request. Should be in milliseconds.\n    - proxy, if you want to pass your request through a http(s) proxy server:\n        - host: eg: \"192.168.0.1\"\n        - port: eg: 8888\n        - protocol: (default: __'http'__ ) can be 'http' or 'https'\n    - rejectUnauthorized: validate certificate for request with HTTPS. [More here](http://nodejs.org/api/https.html#https_https_request_options_callback)\n - callback(err, res): A callback function which is called when the request is complete. __res__ contains the headers ( __res.headers__ ), the http status code ( __res.statusCode__ ) and the body ( __res.body__ )\n\n__Example without extra options__\n\n```js\nvar httpreq = require('httpreq');\n\nhttpreq.post('http://posttestserver.com/post.php', {\n  parameters: {\n    name: 'John',\n    lastname: 'Doe'\n  }\n}, function (err, res){\n  if (err){\n    console.log(err);\n  }else{\n    console.log(res.body);\n  }\n});\n```\n\n__Example with options__\n\n```js\nvar httpreq = require('httpreq');\n\nhttpreq.post('http://posttestserver.com/post.php', {\n  parameters: {\n    name: 'John',\n    lastname: 'Doe'\n  },\n  headers:{\n    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:18.0) Gecko/20100101 Firefox/18.0'\n  },\n  cookies: [\n    'token=DGcGUmplWQSjfqEvmu%2BZA%2Fc',\n    'id=2'\n  ]\n}, function (err, res){\n  if (err){\n    console.log(err);\n  }else{\n    console.log(res.body);\n  }\n});\n```\n\n---------------------------------------\n### httpreq.put(url, [options], callback)\n<a name=\"put\"></a>\n\nSame options as [httpreq.post(url, [options], callback)](#post)\n\n---------------------------------------\n<a name=\"delete\" />\n### httpreq.delete(url, [options], callback)\n\nSame options as [httpreq.post(url, [options], callback)](#post)\n\n---------------------------------------\n<a name=\"options\" />\n### httpreq.options(url, [options], callback)\n\nSame options as [httpreq.get(url, [options], callback)](#get) except for the ability to follow redirects.\n\n---------------------------------------\n<a name=\"upload\" />\n### Uploading files\n\nYou can still use ```httpreq.uploadFiles({url: 'url', files: {}}, callback)```, but it's easier to just use POST (or PUT):\n\n__Example__\n\n```js\nvar httpreq = require('httpreq');\n\nhttpreq.post('http://posttestserver.com/upload.php', {\n  parameters: {\n    name: 'John',\n    lastname: 'Doe'\n  },\n  files:{\n    myfile: __dirname + \"/testupload.jpg\",\n    myotherfile: __dirname + \"/testupload.jpg\"\n  }\n}, function (err, res){\n  if (err) throw err;\n});\n```\n\n---------------------------------------\n<a name=\"binary\"></a>\n### Downloading a binary file\nTo download a binary file, just add __binary: true__ to the options when doing a get or a post.\n\n__Example__\n\n```js\nvar httpreq = require('httpreq');\n\nhttpreq.get('https://ssl.gstatic.com/gb/images/k1_a31af7ac.png', {binary: true}, function (err, res){\n  if (err){\n    console.log(err);\n  }else{\n    fs.writeFile(__dirname + '/test.png', res.body, function (err) {\n      if(err)\n        console.log(\"error writing file\");\n    });\n  }\n});\n```\n\n---------------------------------------\n<a name=\"download\"></a>\n### Downloading a file directly to disk\nTo download a file directly to disk, use the download method provided.\n\nDownloading is done using a stream, so the data is not stored in memory and directly saved to file.\n\n__Example__\n\n```js\nvar httpreq = require('httpreq');\n\nhttpreq.download(\n  'https://ssl.gstatic.com/gb/images/k1_a31af7ac.png',\n  __dirname + '/test.png'\n, function (err, progress){\n  if (err) return console.log(err);\n  console.log(progress);\n}, function (err, res){\n  if (err) return console.log(err);\n  console.log(res);\n});\n\n```\n---------------------------------------\n<a name=\"custombody\"></a>\n### Sending a custom body\nUse the body option to send a custom body (eg. an xml post)\n\n__Example__\n\n```js\nvar httpreq = require('httpreq');\n\nhttpreq.post('http://posttestserver.com/post.php',{\n  body: '<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n  headers:{\n    'Content-Type': 'text/xml',\n  }},\n  function (err, res) {\n    if (err){\n      console.log(err);\n    }else{\n      console.log(res.body);\n    }\n  }\n);\n```\n\n---------------------------------------\n<a name=\"proxy\"></a>\n### Using a http(s) proxy\n\n__Example__\n\n```js\nvar httpreq = require('httpreq');\n\nhttpreq.post('http://posttestserver.com/post.php', {\n  proxy: {\n    host: '10.100.0.126',\n    port: 8888\n  }\n}, function (err, res){\n  if (err){\n    console.log(err);\n  }else{\n    console.log(res.body);\n  }\n});\n```\n\n---------------------------------------\n### httpreq.doRequest(options, callback)\n<a name=\"dorequest\"></a>\n\nhttpreq.doRequest is internally used by httpreq.get() and httpreq.post(). You can use this directly. Everything is stays the same as httpreq.get() or httpreq.post() except that the following options MUST be passed:\n- url: the url to post the files to\n- method: 'GET', 'POST', 'PUT' or 'DELETE'\n\n## Donate\n\nIf you like this module or you want me to update it faster, feel free to donate. It helps increasing my dedication to fixing bugs :-)\n\n[![](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=AB3R2SUL53K7S)\n\n\n",
+  "readmeFilename": "README.md",
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/SamDecrock/node-httpreq.git"
+  },
+  "scripts": {},
+  "version": "0.4.24"
+}
diff --git a/node_modules/httpreq/test/tests.js b/node_modules/httpreq/test/tests.js
new file mode 100644
index 0000000..13735ec
--- /dev/null
+++ b/node_modules/httpreq/test/tests.js
@@ -0,0 +1,307 @@
+var httpreq = require('../lib/httpreq');
+
+var assert = require("assert");
+var expect = require("chai").expect;
+var express = require('express');
+var http = require('http');
+var fs = require('fs');
+
+
+
+describe("httpreq", function(){
+
+	var port, app, webserver, endpointroot;
+
+	before(function (done) {
+		port = Math.floor( Math.random() * (65535 - 1025) + 1025 );
+
+		endpointroot = 'http://localhost:'+port;
+
+		app = express();
+
+		app.configure(function(){
+			app.use(express.logger('dev'));
+			app.use(express.errorHandler());
+			app.use(express.bodyParser());
+			app.use(express.methodOverride());
+			app.use(app.router);
+		});
+
+
+		webserver = http.createServer(app).listen(port, function(){
+			console.log("web server listening on port " + port);
+			done();
+		});
+
+
+	});
+
+	after(function () {
+		webserver.close();
+	});
+
+
+	describe("get", function(){
+
+		it("should do a simple GET request", function (done){
+
+			var path = '/get'; // make sure this is unique when writing tests
+
+			app.get(path, function (req, res) {
+				res.send('ok');
+				done();
+			});
+
+			httpreq.get(endpointroot + path, function (err, res) {
+				if (err) throw err;
+			});
+
+		});
+
+	});
+
+	describe("post", function(){
+
+		it("should do a simple POST request with parameters", function (done){
+
+			var parameters = {
+				name: 'John',
+				lastname: 'Doe'
+			};
+
+			var path = '/post';
+
+			// set up webserver endpoint:
+			app.post(path, function (req, res) {
+				res.send('ok');
+
+				expect(req.body).to.deep.equal(parameters);
+
+				done();
+			});
+
+			// post parameters to webserver endpoint:
+			httpreq.post(endpointroot + path, {
+				parameters: parameters
+			}, function (err, res){
+				if (err) throw err;
+			});
+
+		});
+
+		it("should do a simple POST request with parameters and cookies", function (done){
+
+			var parameters = {
+				name: 'John',
+				lastname: 'Doe'
+			};
+
+			var cookies = [
+				'token=DGcGUmplWQSjfqEvmu%2BZA%2Fc',
+				'id=2'
+			];
+
+			var path = '/postcookies';
+
+			// set up webserver endpoint:
+			app.post(path, function (req, res) {
+				res.send('ok');
+
+				expect(req.body).to.deep.equal(parameters);
+				expect(req.headers.cookie).to.equal(cookies.join('; '));
+
+				done();
+			});
+
+			// post testdata to webserver endpoint:
+			httpreq.post(endpointroot + path, {
+				parameters: parameters,
+				cookies: cookies
+			}, function (err, res){
+				if (err) throw err;
+			});
+
+		});
+
+		it("should do a simple POST request with parameters and custom headers", function (done){
+
+			var parameters = {
+				name: 'John',
+				lastname: 'Doe'
+			};
+
+			var headers = {
+				'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:18.0) Gecko/20100101 Firefox/18.0'
+			};
+
+			var path = '/postheaders';
+
+			// set up webserver endpoint:
+			app.post(path, function (req, res) {
+				res.send('ok');
+
+				expect(req.body).to.deep.equal(parameters);
+				expect(req.headers).to.have.a.property('user-agent', headers['User-Agent']);
+
+				done();
+			});
+
+			// post testdata to webserver endpoint:
+			httpreq.post(endpointroot + path, {
+				parameters: parameters,
+				headers: headers
+			}, function (err, res){
+				if (err) throw err;
+			});
+
+		});
+
+	});
+
+
+	describe("POST json", function () {
+		it('should POST some json', function (done) {
+			var somejson = {
+				name: 'John',
+				lastname: 'Doe'
+			};
+
+			var path = '/postjson';
+
+			// set up webserver endpoint:
+			app.post(path, function (req, res) {
+				res.send('ok');
+
+				expect(req.body).to.deep.equal(somejson);
+
+				done();
+			});
+
+			httpreq.post(endpointroot + path, {
+				json: somejson
+			}, function (err, res){
+				if (err) throw err;
+			});
+		});
+	});
+
+
+	describe("File upload", function () {
+		it('should upload 1 file (old way)', function (done) {
+
+			var testparams = {
+				name: 'John',
+				lastname: 'Doe'
+			};
+
+			var testfile = __dirname + "/testupload.jpg";
+
+			var path = '/uploadfile_old';
+
+			// set up webserver endpoint:
+			app.post(path, function (req, res) {
+				res.send('ok');
+
+				expect(req.body).to.deep.equal(testparams);
+
+				comparefiles(req.files['myfile'].path, testfile, done);
+			});
+
+			httpreq.uploadFiles({
+				url: endpointroot + path,
+				parameters: testparams,
+				files:{
+					myfile: testfile
+				}
+			}, function (err, res){
+				if (err) throw err;
+			});
+		});
+
+		it('should upload 2 files (new way, using POST)', function (done) {
+
+			var testparams = {
+				name: 'John',
+				lastname: 'Doe'
+			};
+
+			var testfile = __dirname + "/testupload.jpg";
+
+			var path = '/uploadfiles';
+
+			// set up webserver endpoint:
+			app.post(path, function (req, res) {
+				res.send('ok');
+
+				expect(req.body).to.deep.equal(testparams);
+
+				comparefiles(req.files['myfile'].path, testfile, function () {
+					comparefiles(req.files['myotherfile'].path, testfile, function () {
+						done();
+					});
+				});
+			});
+
+			httpreq.post(endpointroot + path, {
+				parameters: testparams,
+				files:{
+					myfile: testfile,
+					myotherfile: testfile
+				}
+			}, function (err, res){
+				if (err) throw err;
+			});
+		});
+
+		it('should upload 2 files (new way, using PUT)', function (done) {
+
+			var testparams = {
+				name: 'John',
+				lastname: 'Doe'
+			};
+
+			var testfile = __dirname + "/testupload.jpg";
+
+			var path = '/uploadfiles_put';
+
+			// set up webserver endpoint:
+			app.put(path, function (req, res) {
+				res.send('ok');
+
+				expect(req.body).to.deep.equal(testparams);
+
+				comparefiles(req.files['myfile'].path, testfile, function () {
+					comparefiles(req.files['myotherfile'].path, testfile, function () {
+						done();
+					});
+				});
+			});
+
+			httpreq.put(endpointroot + path, {
+				parameters: testparams,
+				files:{
+					myfile: testfile,
+					myotherfile: testfile
+				}
+			}, function (err, res){
+				if (err) throw err;
+			});
+		});
+	});
+
+});
+
+
+function comparefiles (file1, file2, callback) {
+	fs.readFile(file1, function (err, file1data) {
+		if(err) throw err;
+
+		fs.readFile(file2, function (err, file2data) {
+			if(err) throw err;
+
+			 expect(file1data).to.deep.equal(file2data);
+
+			 callback();
+		});
+	});
+}
\ No newline at end of file
diff --git a/node_modules/httpreq/test/testupload.jpg b/node_modules/httpreq/test/testupload.jpg
new file mode 100644
index 0000000..a00d8bc
Binary files /dev/null and b/node_modules/httpreq/test/testupload.jpg differ
diff --git a/node_modules/nodemailer-fetch/.eslintrc.js b/node_modules/nodemailer-fetch/.eslintrc.js
new file mode 100644
index 0000000..2624fa3
--- /dev/null
+++ b/node_modules/nodemailer-fetch/.eslintrc.js
@@ -0,0 +1,56 @@
+'use strict';
+
+module.exports = {
+    rules: {
+        indent: [2, 4, {
+            SwitchCase: 1
+        }],
+        quotes: [2, 'single'],
+        'linebreak-style': [2, 'unix'],
+        semi: [2, 'always'],
+        strict: [2, 'global'],
+        eqeqeq: 2,
+        'dot-notation': 2,
+        curly: 2,
+        'no-fallthrough': 2,
+        'quote-props': [2, 'as-needed'],
+        'no-unused-expressions': [2, {
+            allowShortCircuit: true
+        }],
+        'no-unused-vars': 2,
+        'no-undef': 2,
+        'handle-callback-err': 2,
+        'no-new': 2,
+        'new-cap': 2,
+        'no-eval': 2,
+        'no-invalid-this': 2,
+        radix: [2, 'always'],
+        'no-use-before-define': [2, 'nofunc'],
+        'callback-return': [2, ['callback', 'cb', 'done']],
+        'comma-dangle': [2, 'never'],
+        'comma-style': [2, 'last'],
+        'no-regex-spaces': 2,
+        'no-empty': 2,
+        'no-duplicate-case': 2,
+        'no-empty-character-class': 2,
+        'no-redeclare': [2, {
+            builtinGlobals: true
+        }],
+        'block-scoped-var': 2,
+        'no-sequences': 2,
+        'no-throw-literal': 2,
+        'no-useless-concat': 2,
+        'no-void': 2,
+        yoda: 2,
+        'no-bitwise': 2,
+        'no-lonely-if': 2,
+        'no-mixed-spaces-and-tabs': 2,
+        'no-console': 0
+    },
+    env: {
+        es6: false,
+        node: true
+    },
+    extends: 'eslint:recommended',
+    fix: true
+};
diff --git a/node_modules/nodemailer-fetch/.npmignore b/node_modules/nodemailer-fetch/.npmignore
new file mode 100644
index 0000000..2f6141d
--- /dev/null
+++ b/node_modules/nodemailer-fetch/.npmignore
@@ -0,0 +1,3 @@
+node_modules
+npm-debug.log
+.DS_Store
\ No newline at end of file
diff --git a/node_modules/nodemailer-fetch/.travis.yml b/node_modules/nodemailer-fetch/.travis.yml
new file mode 100644
index 0000000..31eef17
--- /dev/null
+++ b/node_modules/nodemailer-fetch/.travis.yml
@@ -0,0 +1,19 @@
+language: node_js
+sudo: false
+node_js:
+  - "0.10"
+  - 0.12
+  - iojs
+  - '4'
+  - '5'
+before_install:
+  - npm install -g grunt-cli
+notifications:
+  email:
+    - andris@kreata.ee
+  webhooks:
+    urls:
+      - https://webhooks.gitter.im/e/0ed18fd9b3e529b3c2cc
+    on_success: change  # options: [always|never|change] default: always
+    on_failure: always  # options: [always|never|change] default: always
+    on_start: false     # default: false
diff --git a/node_modules/nodemailer-fetch/CHANGELOG.md b/node_modules/nodemailer-fetch/CHANGELOG.md
new file mode 100644
index 0000000..a57a4b0
--- /dev/null
+++ b/node_modules/nodemailer-fetch/CHANGELOG.md
@@ -0,0 +1,30 @@
+# Changelog
+
+## v1.6.0 2016-08-18
+
+  * Added new option `headers`
+
+## v1.5.0 2016-08-18
+
+  * Allow streams as POST body
+
+## v1.3.0 2016-02-11
+
+  * Added new option `timeout`
+
+## v1.2.1 2016-01-18
+
+  * Enclose http.request into try..catch to get url parse errors
+
+## v1.2.0 2016-01-18
+
+  * Export `Cookies` constructor
+
+## v1.1.0 2016-01-18
+
+  * Exposed `options` object
+  * Added new options `maxRedirects`, `userAgent` and `cookie`
+
+## v1.0.0 2015-12-30
+
+  * Initial version
diff --git a/node_modules/nodemailer-fetch/Gruntfile.js b/node_modules/nodemailer-fetch/Gruntfile.js
new file mode 100644
index 0000000..1b88860
--- /dev/null
+++ b/node_modules/nodemailer-fetch/Gruntfile.js
@@ -0,0 +1,27 @@
+'use strict';
+
+module.exports = function (grunt) {
+
+    // Project configuration.
+    grunt.initConfig({
+        eslint: {
+            all: ['lib/*.js', 'test/*.js', 'Gruntfile.js', '.eslintrc.js']
+        },
+
+        mochaTest: {
+            all: {
+                options: {
+                    reporter: 'spec'
+                },
+                src: ['test/*-test.js']
+            }
+        }
+    });
+
+    // Load the plugin(s)
+    grunt.loadNpmTasks('grunt-eslint');
+    grunt.loadNpmTasks('grunt-mocha-test');
+
+    // Tasks
+    grunt.registerTask('default', ['eslint', 'mochaTest']);
+};
diff --git a/node_modules/nodemailer-fetch/LICENSE b/node_modules/nodemailer-fetch/LICENSE
new file mode 100644
index 0000000..ed2ed2d
--- /dev/null
+++ b/node_modules/nodemailer-fetch/LICENSE
@@ -0,0 +1,16 @@
+Copyright (c) 2015-2016 Andris Reinman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/node_modules/nodemailer-fetch/README.md b/node_modules/nodemailer-fetch/README.md
new file mode 100644
index 0000000..c0d2ced
--- /dev/null
+++ b/node_modules/nodemailer-fetch/README.md
@@ -0,0 +1,55 @@
+# nodemailer-fetch
+
+Fetches HTTP URL contents for [nodemailer](https://github.com/nodemailer/nodemailer).
+
+[![Build Status](https://secure.travis-ci.org/nodemailer/nodemailer-fetch.svg)](http://travis-ci.org/nodemailer/nodemailer-fetch)
+<a href="http://badge.fury.io/js/nodemailer-fetch"><img src="https://badge.fury.io/js/nodemailer-fetch.svg" alt="NPM version" height="18"></a>
+
+## Usage
+
+```javascript
+var fetch = require('nodemailer-fetch');
+fetch('http://www.google.com/').pipe(process.stdout);
+```
+
+The method takes the destination URL as the first and optional options object as the second argument.
+
+The defaults are the following:
+
+  * Default method is GET
+  * Basic auth is supported
+  * Up to 5 redirects are followed (Basic auth gets lost after first redirect)
+  * gzip is handled if present
+  * Cookies are supported
+  * No shared HTTP Agent
+  * Invalid SSL certs are allowed. Can be overwritten with the `tls` option
+
+### options
+
+Possible options are the following:
+
+  * **userAgent** a string defining the User Agent of the request (by default not set)
+  * **cookie** a cookie string or an array of cookie strings where a cookie is the value used by 'Set-Cookie' header
+  * **maxRedirects** how many redirects to allow (defaults to 5, set to 0 to disable redirects entirely)
+  * **method** HTTP method to use, defaults to GET (if `body` is set defaults to POST)
+  * **body** HTTP payload to send. If the value is an object it is converted to an *x-www-form-urlencoded* payload, other values are passed as is. Unlike authentication data payload and method is preserved between redirects
+  * **contentType** optional content type for the HTTP payload. Defaults to *x-www-form-urlencoded*. If the value is `false` then Content-Type header is not set
+  * **tls** optional object of TLS options
+  * **timeout** (milliseconds) sets timeout for the connection. Returns an error if timeout occurs
+  * **headers** custom headers as an object where key is the header key and value is either a string or an array of strings for multiple values
+
+  ```javascript
+  var fetch = require('nodemailer-fetch');
+  fetch('http://www.google.com/', {
+      cookie: [
+          'cookie_name1=cookie_value1',
+          'cookie_name2=cookie_value2; expires=Sun, 16 Jul 3567 06:23:41 GMT',
+      ],
+      userAgent: 'MyFetcher/1.0'
+  }).pipe(process.stdout);
+  ```
+
+> Cookies are domain specific like normal browser cookies, so if a redirect happens to another domain, then cookies are not passed to it, HTTPS-only cookies are not passed to HTTP etc.
+
+## License
+**MIT**
diff --git a/node_modules/nodemailer-fetch/lib/cookies.js b/node_modules/nodemailer-fetch/lib/cookies.js
new file mode 100644
index 0000000..4f6ff84
--- /dev/null
+++ b/node_modules/nodemailer-fetch/lib/cookies.js
@@ -0,0 +1,275 @@
+'use strict';
+
+// module to handle cookies
+
+var urllib = require('url');
+
+var SESSION_TIMEOUT = 1800; // 30 min
+
+module.exports = Cookies;
+
+/**
+ * Creates a biskviit cookie jar for managing cookie values in memory
+ *
+ * @constructor
+ * @param {Object} [options] Optional options object
+ */
+function Cookies(options) {
+    this.options = options || {};
+    this.cookies = [];
+}
+
+/**
+ * Stores a cookie string to the cookie storage
+ *
+ * @param {String} cookieStr Value from the 'Set-Cookie:' header
+ * @param {String} url Current URL
+ */
+Cookies.prototype.set = function (cookieStr, url) {
+    var urlparts = urllib.parse(url || '');
+    var cookie = this.parse(cookieStr);
+    var domain;
+
+    if (cookie.domain) {
+        domain = cookie.domain.replace(/^\./, '');
+
+        // do not allow cross origin cookies
+        if (
+            // can't be valid if the requested domain is shorter than current hostname
+            urlparts.hostname.length < domain.length ||
+
+            // prefix domains with dot to be sure that partial matches are not used
+            ('.' + urlparts.hostname).substr(-domain.length + 1) !== ('.' + domain)) {
+            cookie.domain = urlparts.hostname;
+        }
+    } else {
+        cookie.domain = urlparts.hostname;
+    }
+
+    if (!cookie.path) {
+        cookie.path = this.getPath(urlparts.pathname);
+    }
+
+    // if no expire date, then use sessionTimeout value
+    if (!cookie.expires) {
+        cookie.expires = new Date(Date.now() + (Number(this.options.sessionTimeout || SESSION_TIMEOUT) || SESSION_TIMEOUT) * 1000);
+    }
+
+    return this.add(cookie);
+};
+
+/**
+ * Returns cookie string for the 'Cookie:' header.
+ *
+ * @param {String} url URL to check for
+ * @returns {String} Cookie header or empty string if no matches were found
+ */
+Cookies.prototype.get = function (url) {
+    return this.list(url).map(function (cookie) {
+        return cookie.name + '=' + cookie.value;
+    }).join('; ');
+};
+
+/**
+ * Lists all valied cookie objects for the specified URL
+ *
+ * @param {String} url URL to check for
+ * @returns {Array} An array of cookie objects
+ */
+Cookies.prototype.list = function (url) {
+    var result = [];
+    var i;
+    var cookie;
+
+    for (i = this.cookies.length - 1; i >= 0; i--) {
+        cookie = this.cookies[i];
+
+        if (this.isExpired(cookie)) {
+            this.cookies.splice(i, i);
+            continue;
+        }
+
+        if (this.match(cookie, url)) {
+            result.unshift(cookie);
+        }
+    }
+
+    return result;
+};
+
+/**
+ * Parses cookie string from the 'Set-Cookie:' header
+ *
+ * @param {String} cookieStr String from the 'Set-Cookie:' header
+ * @returns {Object} Cookie object
+ */
+Cookies.prototype.parse = function (cookieStr) {
+    var cookie = {};
+
+    (cookieStr || '').toString().split(';').forEach(function (cookiePart) {
+        var valueParts = cookiePart.split('=');
+        var key = valueParts.shift().trim().toLowerCase();
+        var value = valueParts.join('=').trim();
+        var domain;
+
+        if (!key) {
+            // skip empty parts
+            return;
+        }
+
+        switch (key) {
+
+            case 'expires':
+                value = new Date(value);
+                // ignore date if can not parse it
+                if (value.toString() !== 'Invalid Date') {
+                    cookie.expires = value;
+                }
+                break;
+
+            case 'path':
+                cookie.path = value;
+                break;
+
+            case 'domain':
+                domain = value.toLowerCase();
+                if (domain.length && domain.charAt(0) !== '.') {
+                    domain = '.' + domain; // ensure preceeding dot for user set domains
+                }
+                cookie.domain = domain;
+                break;
+
+            case 'max-age':
+                cookie.expires = new Date(Date.now() + (Number(value) || 0) * 1000);
+                break;
+
+            case 'secure':
+                cookie.secure = true;
+                break;
+
+            case 'httponly':
+                cookie.httponly = true;
+                break;
+
+            default:
+                if (!cookie.name) {
+                    cookie.name = key;
+                    cookie.value = value;
+                }
+        }
+    });
+
+    return cookie;
+};
+
+/**
+ * Checks if a cookie object is valid for a specified URL
+ *
+ * @param {Object} cookie Cookie object
+ * @param {String} url URL to check for
+ * @returns {Boolean} true if cookie is valid for specifiec URL
+ */
+Cookies.prototype.match = function (cookie, url) {
+    var urlparts = urllib.parse(url || '');
+
+    // check if hostname matches
+    // .foo.com also matches subdomains, foo.com does not
+    if (urlparts.hostname !== cookie.domain && (cookie.domain.charAt(0) !== '.' || ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
+        return false;
+    }
+
+    // check if path matches
+    var path = this.getPath(urlparts.pathname);
+    if (path.substr(0, cookie.path.length) !== cookie.path) {
+        return false;
+    }
+
+    // check secure argument
+    if (cookie.secure && urlparts.protocol !== 'https:') {
+        return false;
+    }
+
+    return true;
+};
+
+/**
+ * Adds (or updates/removes if needed) a cookie object to the cookie storage
+ *
+ * @param {Object} cookie Cookie value to be stored
+ */
+Cookies.prototype.add = function (cookie) {
+    var i;
+    var len;
+
+    // nothing to do here
+    if (!cookie || !cookie.name) {
+        return false;
+    }
+
+    // overwrite if has same params
+    for (i = 0, len = this.cookies.length; i < len; i++) {
+        if (this.compare(this.cookies[i], cookie)) {
+
+            // check if the cookie needs to be removed instead
+            if (this.isExpired(cookie)) {
+                this.cookies.splice(i, 1); // remove expired/unset cookie
+                return false;
+            }
+
+            this.cookies[i] = cookie;
+            return true;
+        }
+    }
+
+    // add as new if not already expired
+    if (!this.isExpired(cookie)) {
+        this.cookies.push(cookie);
+    }
+
+    return true;
+};
+
+/**
+ * Checks if two cookie objects are the same
+ *
+ * @param {Object} a Cookie to check against
+ * @param {Object} b Cookie to check against
+ * @returns {Boolean} True, if the cookies are the same
+ */
+Cookies.prototype.compare = function (a, b) {
+    return a.name === b.name && a.path === b.path && a.domain === b.domain && a.secure === b.secure && a.httponly === a.httponly;
+};
+
+/**
+ * Checks if a cookie is expired
+ *
+ * @param {Object} cookie Cookie object to check against
+ * @returns {Boolean} True, if the cookie is expired
+ */
+Cookies.prototype.isExpired = function (cookie) {
+    return (cookie.expires && cookie.expires < new Date()) || !cookie.value;
+};
+
+/**
+ * Returns normalized cookie path for an URL path argument
+ *
+ * @param {String} pathname
+ * @returns {String} Normalized path
+ */
+Cookies.prototype.getPath = function (pathname) {
+    var path = (pathname || '/').split('/');
+    path.pop(); // remove filename part
+    path = path.join('/').trim();
+
+    // ensure path prefix /
+    if (path.charAt(0) !== '/') {
+        path = '/' + path;
+    }
+
+    // ensure path suffix /
+    if (path.substr(-1) !== '/') {
+        path += '/';
+    }
+
+    return path;
+};
diff --git a/node_modules/nodemailer-fetch/lib/fetch.js b/node_modules/nodemailer-fetch/lib/fetch.js
new file mode 100644
index 0000000..3b4423e
--- /dev/null
+++ b/node_modules/nodemailer-fetch/lib/fetch.js
@@ -0,0 +1,224 @@
+'use strict';
+
+var http = require('http');
+var https = require('https');
+var urllib = require('url');
+var zlib = require('zlib');
+var PassThrough = require('stream').PassThrough;
+var Cookies = require('./cookies');
+
+var MAX_REDIRECTS = 5;
+
+module.exports = function (url, options) {
+    return fetch(url, options);
+};
+
+module.exports.Cookies = Cookies;
+
+function fetch(url, options) {
+    options = options || {};
+
+    options.fetchRes = options.fetchRes || new PassThrough();
+    options.cookies = options.cookies || new Cookies();
+    options.redirects = options.redirects || 0;
+    options.maxRedirects = isNaN(options.maxRedirects) ? MAX_REDIRECTS : options.maxRedirects;
+
+    if (options.cookie) {
+        [].concat(options.cookie || []).forEach(function (cookie) {
+            options.cookies.set(cookie, url);
+        });
+        options.cookie = false;
+    }
+
+    var fetchRes = options.fetchRes;
+    var parsed = urllib.parse(url);
+    var method = (options.method || '').toString().trim().toUpperCase() || 'GET';
+    var finished = false;
+    var cookies;
+    var body;
+
+    var handler = parsed.protocol === 'https:' ? https : http;
+
+    var headers = {
+        'accept-encoding': 'gzip,deflate'
+    };
+
+    Object.keys(options.headers || {}).forEach(function (key) {
+        headers[key.toLowerCase().trim()] = options.headers[key];
+    });
+
+    if (options.userAgent) {
+        headers['User-Agent'] = options.userAgent;
+    }
+
+    if (parsed.auth) {
+        headers.Authorization = 'Basic ' + new Buffer(parsed.auth).toString('base64');
+    }
+
+    if ((cookies = options.cookies.get(url))) {
+        headers.cookie = cookies;
+    }
+
+    if (options.body) {
+        if (options.contentType !== false) {
+            headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded';
+        }
+
+        if (typeof options.body.pipe === 'function') {
+            // it's a stream
+            headers['Transfer-Encoding'] = 'chunked';
+            body = options.body;
+            body.on('error', function (err) {
+                if (finished) {
+                    return;
+                }
+                finished = true;
+                fetchRes.emit('error', err);
+            });
+        } else {
+            if (options.body instanceof Buffer) {
+                body = options.body;
+            } else if (typeof options.body === 'object') {
+                body = new Buffer(Object.keys(options.body).map(function (key) {
+                    var value = options.body[key].toString().trim();
+                    return encodeURIComponent(key) + '=' + encodeURIComponent(value);
+                }).join('&'));
+            } else {
+                body = new Buffer(options.body.toString().trim());
+            }
+
+            headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded';
+            headers['Content-Length'] = body.length;
+        }
+        // if method is not provided, use POST instead of GET
+        method = (options.method || '').toString().trim().toUpperCase() || 'POST';
+    }
+
+    var req;
+    var reqOptions = {
+        method: method,
+        host: parsed.hostname,
+        path: parsed.path,
+        port: parsed.port ? parsed.port : (parsed.protocol === 'https:' ? 443 : 80),
+        headers: headers,
+        rejectUnauthorized: false,
+        agent: false
+    };
+
+    if (options.tls) {
+        Object.keys(options.tls).forEach(function (key) {
+            reqOptions[key] = options.tls[key];
+        });
+    }
+
+    try {
+        req = handler.request(reqOptions);
+    } catch (E) {
+        finished = true;
+        setImmediate(function () {
+            fetchRes.emit('error', E);
+        });
+        return fetchRes;
+    }
+
+    if (options.timeout) {
+        req.setTimeout(options.timeout, function () {
+            if (finished) {
+                return;
+            }
+            finished = true;
+            req.abort();
+            fetchRes.emit('error', new Error('Request Tiemout'));
+        });
+    }
+
+    req.on('error', function (err) {
+        if (finished) {
+            return;
+        }
+        finished = true;
+        fetchRes.emit('error', err);
+    });
+
+    req.on('response', function (res) {
+        var inflate;
+
+        if (finished) {
+            return;
+        }
+
+        switch (res.headers['content-encoding']) {
+            case 'gzip':
+            case 'deflate':
+                inflate = zlib.createUnzip();
+                break;
+        }
+
+        if (res.headers['set-cookie']) {
+            [].concat(res.headers['set-cookie'] || []).forEach(function (cookie) {
+                options.cookies.set(cookie, url);
+            });
+        }
+
+        if ([301, 302, 303, 307, 308].indexOf(res.statusCode) >= 0 && res.headers.location) {
+            // redirect
+            options.redirects++;
+            if (options.redirects > options.maxRedirects) {
+                finished = true;
+                fetchRes.emit('error', new Error('Maximum redirect count exceeded'));
+                req.abort();
+                return;
+            }
+            return fetch(urllib.resolve(url, res.headers.location), options);
+        }
+
+        if (res.statusCode >= 300) {
+            finished = true;
+            fetchRes.emit('error', new Error('Invalid status code ' + res.statusCode));
+            req.abort();
+            return;
+        }
+
+        res.on('error', function (err) {
+            if (finished) {
+                return;
+            }
+            finished = true;
+            fetchRes.emit('error', err);
+            req.abort();
+        });
+
+        if (inflate) {
+            res.pipe(inflate).pipe(fetchRes);
+            inflate.on('error', function (err) {
+                if (finished) {
+                    return;
+                }
+                finished = true;
+                fetchRes.emit('error', err);
+                req.abort();
+            });
+        } else {
+            res.pipe(fetchRes);
+        }
+    });
+
+    setImmediate(function () {
+        if (body) {
+            try {
+                if (typeof body.pipe === 'function') {
+                    return body.pipe(req);
+                } else {
+                    req.write(body);
+                }
+            } catch (err) {
+                finished = true;
+                fetchRes.emit('error', err);
+                return;
+            }
+        }
+        req.end();
+    });
+
+    return fetchRes;
+}
diff --git a/node_modules/nodemailer-fetch/package.json b/node_modules/nodemailer-fetch/package.json
new file mode 100644
index 0000000..7eb7f81
--- /dev/null
+++ b/node_modules/nodemailer-fetch/package.json
@@ -0,0 +1,94 @@
+{
+  "_args": [
+    [
+      {
+        "raw": "nodemailer-fetch@1.6.0",
+        "scope": null,
+        "escapedName": "nodemailer-fetch",
+        "name": "nodemailer-fetch",
+        "rawSpec": "1.6.0",
+        "spec": "1.6.0",
+        "type": "version"
+      },
+      "/Users/fzy/project/koa2_Sequelize_project/node_modules/nodemailer-shared"
+    ]
+  ],
+  "_from": "nodemailer-fetch@1.6.0",
+  "_id": "nodemailer-fetch@1.6.0",
+  "_inCache": true,
+  "_location": "/nodemailer-fetch",
+  "_nodeVersion": "6.3.1",
+  "_npmOperationalInternal": {
+    "host": "packages-12-west.internal.npmjs.com",
+    "tmp": "tmp/nodemailer-fetch-1.6.0.tgz_1471509114442_0.5888715244363993"
+  },
+  "_npmUser": {
+    "name": "andris",
+    "email": "andris@kreata.ee"
+  },
+  "_npmVersion": "3.10.3",
+  "_phantomChildren": {},
+  "_requested": {
+    "raw": "nodemailer-fetch@1.6.0",
+    "scope": null,
+    "escapedName": "nodemailer-fetch",
+    "name": "nodemailer-fetch",
+    "rawSpec": "1.6.0",
+    "spec": "1.6.0",
+    "type": "version"
+  },
+  "_requiredBy": [
+    "/nodemailer-shared"
+  ],
+  "_resolved": "https://registry.npmjs.org/nodemailer-fetch/-/nodemailer-fetch-1.6.0.tgz",
+  "_shasum": "79c4908a1c0f5f375b73fe888da9828f6dc963a4",
+  "_shrinkwrap": null,
+  "_spec": "nodemailer-fetch@1.6.0",
+  "_where": "/Users/fzy/project/koa2_Sequelize_project/node_modules/nodemailer-shared",
+  "author": {
+    "name": "Andris Reinman"
+  },
+  "bugs": {
+    "url": "https://github.com/nodemailer/nodemailer-fetch/issues"
+  },
+  "dependencies": {},
+  "description": "GET HTTP contents",
+  "devDependencies": {
+    "chai": "^3.5.0",
+    "grunt": "^1.0.1",
+    "grunt-eslint": "^19.0.0",
+    "grunt-mocha-test": "^0.12.7",
+    "mocha": "^3.0.2"
+  },
+  "directories": {},
+  "dist": {
+    "shasum": "79c4908a1c0f5f375b73fe888da9828f6dc963a4",
+    "tarball": "https://registry.npmjs.org/nodemailer-fetch/-/nodemailer-fetch-1.6.0.tgz"
+  },
+  "gitHead": "581580f1b21c61c10a3296c249f90d436fac8926",
+  "homepage": "https://github.com/nodemailer/nodemailer-fetch#readme",
+  "keywords": [
+    "nodemailer",
+    "http"
+  ],
+  "license": "MIT",
+  "main": "lib/fetch.js",
+  "maintainers": [
+    {
+      "name": "andris",
+      "email": "andris@kreata.ee"
+    }
+  ],
+  "name": "nodemailer-fetch",
+  "optionalDependencies": {},
+  "readme": "# nodemailer-fetch\n\nFetches HTTP URL contents for [nodemailer](https://github.com/nodemailer/nodemailer).\n\n[![Build Status](https://secure.travis-ci.org/nodemailer/nodemailer-fetch.svg)](http://travis-ci.org/nodemailer/nodemailer-fetch)\n<a href=\"http://badge.fury.io/js/nodemailer-fetch\"><img src=\"https://badge.fury.io/js/nodemailer-fetch.svg\" alt=\"NPM version\" height=\"18\"></a>\n\n## Usage\n\n```javascript\nvar fetch = require('nodemailer-fetch');\nfetch('http://www.google.com/').pipe(process.stdout);\n```\n\nThe method takes the destination URL as the first and optional options object as the second argument.\n\nThe defaults are the following:\n\n  * Default method is GET\n  * Basic auth is supported\n  * Up to 5 redirects are followed (Basic auth gets lost after first redirect)\n  * gzip is handled if present\n  * Cookies are supported\n  * No shared HTTP Agent\n  * Invalid SSL certs are allowed. Can be overwritten with the `tls` option\n\n### options\n\nPossible options are the following:\n\n  * **userAgent** a string defining the User Agent of the request (by default not set)\n  * **cookie** a cookie string or an array of cookie strings where a cookie is the value used by 'Set-Cookie' header\n  * **maxRedirects** how many redirects to allow (defaults to 5, set to 0 to disable redirects entirely)\n  * **method** HTTP method to use, defaults to GET (if `body` is set defaults to POST)\n  * **body** HTTP payload to send. If the value is an object it is converted to an *x-www-form-urlencoded* payload, other values are passed as is. Unlike authentication data payload and method is preserved between redirects\n  * **contentType** optional content type for the HTTP payload. Defaults to *x-www-form-urlencoded*. If the value is `false` then Content-Type header is not set\n  * **tls** optional object of TLS options\n  * **timeout** (milliseconds) sets timeout for the connection. Returns an error if timeout occurs\n  * **headers** custom headers as an object where key is the header key and value is either a string or an array of strings for multiple values\n\n  ```javascript\n  var fetch = require('nodemailer-fetch');\n  fetch('http://www.google.com/', {\n      cookie: [\n          'cookie_name1=cookie_value1',\n          'cookie_name2=cookie_value2; expires=Sun, 16 Jul 3567 06:23:41 GMT',\n      ],\n      userAgent: 'MyFetcher/1.0'\n  }).pipe(process.stdout);\n  ```\n\n> Cookies are domain specific like normal browser cookies, so if a redirect happens to another domain, then cookies are not passed to it, HTTPS-only cookies are not passed to HTTP etc.\n\n## License\n**MIT**\n",
+  "readmeFilename": "README.md",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/nodemailer/nodemailer-fetch.git"
+  },
+  "scripts": {
+    "test": "grunt mochaTest"
+  },
+  "version": "1.6.0"
+}
diff --git a/node_modules/nodemailer-fetch/test/cookies-test.js b/node_modules/nodemailer-fetch/test/cookies-test.js
new file mode 100644
index 0000000..00af46a
--- /dev/null
+++ b/node_modules/nodemailer-fetch/test/cookies-test.js
@@ -0,0 +1,391 @@
+/* eslint no-unused-expressions:0 */
+/* globals beforeEach, describe, it */
+
+'use strict';
+
+var chai = require('chai');
+var expect = chai.expect;
+
+//var http = require('http');
+var Cookies = require('../lib/cookies');
+
+chai.config.includeStack = true;
+
+describe('Cookies Unit Tests', function () {
+    var biskviit;
+
+    beforeEach(function () {
+        biskviit = new Cookies();
+    });
+
+    describe('#getPath', function () {
+
+        it('should return root path', function () {
+            expect(biskviit.getPath('/')).to.equal('/');
+            expect(biskviit.getPath('')).to.equal('/');
+            expect(biskviit.getPath('/index.php')).to.equal('/');
+        });
+
+        it('should return without file', function () {
+            expect(biskviit.getPath('/path/to/file')).to.equal('/path/to/');
+        });
+
+    });
+
+    describe('#isExpired', function () {
+        it('should match expired cookie', function () {
+            expect(biskviit.isExpired({
+                name: 'a',
+                value: 'b',
+                expires: new Date(Date.now() + 10000)
+            })).to.be.false;
+
+            expect(biskviit.isExpired({
+                name: 'a',
+                value: '',
+                expires: new Date(Date.now() + 10000)
+            })).to.be.true;
+
+            expect(biskviit.isExpired({
+                name: 'a',
+                value: 'b',
+                expires: new Date(Date.now() - 10000)
+            })).to.be.true;
+        });
+    });
+
+    describe('#compare', function () {
+        it('should match similar cookies', function () {
+            expect(biskviit.compare({
+                name: 'zzz',
+                path: '/',
+                domain: 'example.com',
+                secure: false,
+                httponly: false
+            }, {
+                name: 'zzz',
+                path: '/',
+                domain: 'example.com',
+                secure: false,
+                httponly: false
+            })).to.be.true;
+
+            expect(biskviit.compare({
+                name: 'zzz',
+                path: '/',
+                domain: 'example.com',
+                secure: false,
+                httponly: false
+            }, {
+                name: 'yyy',
+                path: '/',
+                domain: 'example.com',
+                secure: false,
+                httponly: false
+            })).to.be.false;
+
+            expect(biskviit.compare({
+                name: 'zzz',
+                path: '/',
+                domain: 'example.com',
+                secure: false,
+                httponly: false
+            }, {
+                name: 'zzz',
+                path: '/amp',
+                domain: 'example.com',
+                secure: false,
+                httponly: false
+            })).to.be.false;
+
+            expect(biskviit.compare({
+                name: 'zzz',
+                path: '/',
+                domain: 'example.com',
+                secure: false,
+                httponly: false
+            }, {
+                name: 'zzz',
+                path: '/',
+                domain: 'examples.com',
+                secure: false,
+                httponly: false
+            })).to.be.false;
+
+            expect(biskviit.compare({
+                name: 'zzz',
+                path: '/',
+                domain: 'example.com',
+                secure: false,
+                httponly: false
+            }, {
+                name: 'zzz',
+                path: '/',
+                domain: 'example.com',
+                secure: true,
+                httponly: false
+            })).to.be.false;
+        });
+    });
+
+    describe('#add', function () {
+        it('should append new cookie', function () {
+            expect(biskviit.cookies.length).to.equal(0);
+            biskviit.add({
+                name: 'zzz',
+                value: 'abc',
+                path: '/',
+                expires: new Date(Date.now() + 10000),
+                domain: 'example.com',
+                secure: false,
+                httponly: false
+            });
+            expect(biskviit.cookies.length).to.equal(1);
+            expect(biskviit.cookies[0].name).to.equal('zzz');
+            expect(biskviit.cookies[0].value).to.equal('abc');
+        });
+
+        it('should update existing cookie', function () {
+            expect(biskviit.cookies.length).to.equal(0);
+            biskviit.add({
+                name: 'zzz',
+                value: 'abc',
+                path: '/',
+                expires: new Date(Date.now() + 10000),
+                domain: 'example.com',
+                secure: false,
+                httponly: false
+            });
+            biskviit.add({
+                name: 'zzz',
+                value: 'def',
+                path: '/',
+                expires: new Date(Date.now() + 10000),
+                domain: 'example.com',
+                secure: false,
+                httponly: false
+            });
+            expect(biskviit.cookies.length).to.equal(1);
+            expect(biskviit.cookies[0].name).to.equal('zzz');
+            expect(biskviit.cookies[0].value).to.equal('def');
+        });
+    });
+
+    describe('#match', function () {
+        it('should check if a cookie matches particular domain and path', function () {
+            var cookie = {
+                name: 'zzz',
+                value: 'abc',
+                path: '/def/',
+                expires: new Date(Date.now() + 10000),
+                domain: 'example.com',
+                secure: false,
+                httponly: false
+            };
+            expect(biskviit.match(cookie, 'http://example.com/def/')).to.be.true;
+            expect(biskviit.match(cookie, 'http://example.com/bef/')).to.be.false;
+        });
+
+        it('should check if a cookie matches particular domain and path', function () {
+            var cookie = {
+                name: 'zzz',
+                value: 'abc',
+                path: '/def',
+                expires: new Date(Date.now() + 10000),
+                domain: 'example.com',
+                secure: false,
+                httponly: false
+            };
+            expect(biskviit.match(cookie, 'http://example.com/def/')).to.be.true;
+            expect(biskviit.match(cookie, 'http://example.com/bef/')).to.be.false;
+        });
+
+        it('should check if a cookie is secure', function () {
+            var cookie = {
+                name: 'zzz',
+                value: 'abc',
+                path: '/def/',
+                expires: new Date(Date.now() + 10000),
+                domain: 'example.com',
+                secure: true,
+                httponly: false
+            };
+            expect(biskviit.match(cookie, 'https://example.com/def/')).to.be.true;
+            expect(biskviit.match(cookie, 'http://example.com/def/')).to.be.false;
+        });
+    });
+
+    describe('#parse', function () {
+        it('should parse Set-Cookie value', function () {
+
+            expect(biskviit.parse('theme=plain')).to.deep.equal({
+                name: 'theme',
+                value: 'plain'
+            });
+
+            expect(biskviit.parse('SSID=Ap4P….GTEq; Domain=foo.com; Path=/; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly')).to.deep.equal({
+                name: 'ssid',
+                value: 'Ap4P….GTEq',
+                domain: '.foo.com',
+                path: '/',
+                httponly: true,
+                secure: true,
+                expires: new Date('Wed, 13 Jan 2021 22:23:01 GMT')
+            });
+
+        });
+
+        it('should ignore invalid expire header', function () {
+            expect(biskviit.parse('theme=plain; Expires=Wed, 13 Jan 2021 22:23:01 GMT')).to.deep.equal({
+                name: 'theme',
+                value: 'plain',
+                expires: new Date('Wed, 13 Jan 2021 22:23:01 GMT')
+            });
+
+            expect(biskviit.parse('theme=plain; Expires=ZZZZZZZZ GMT')).to.deep.equal({
+                name: 'theme',
+                value: 'plain'
+            });
+        });
+    });
+
+    describe('Listing', function () {
+        beforeEach(function () {
+            biskviit.cookies = [{
+                name: 'ssid1',
+                value: 'Ap4P….GTEq1',
+                domain: '.foo.com',
+                path: '/',
+                httponly: true,
+                secure: true,
+                expires: new Date('Wed, 13 Jan 2021 22:23:01 GMT')
+            }, {
+                name: 'ssid2',
+                value: 'Ap4P….GTEq2',
+                domain: '.foo.com',
+                path: '/',
+                httponly: true,
+                secure: true,
+                expires: new Date('Wed, 13 Jan 1900 22:23:01 GMT')
+            }, {
+                name: 'ssid3',
+                value: 'Ap4P….GTEq3',
+                domain: 'foo.com',
+                path: '/',
+                httponly: true,
+                secure: true,
+                expires: new Date('Wed, 13 Jan 2021 22:23:01 GMT')
+            }, {
+                name: 'ssid4',
+                value: 'Ap4P….GTEq4',
+                domain: 'www.foo.com',
+                path: '/',
+                httponly: true,
+                secure: true,
+                expires: new Date('Wed, 13 Jan 2021 22:23:01 GMT')
+            }, {
+                name: 'ssid5',
+                value: 'Ap4P….GTEq5',
+                domain: 'broo.com',
+                path: '/',
+                httponly: true,
+                secure: true,
+                expires: new Date('Wed, 13 Jan 2021 22:23:01 GMT')
+            }];
+        });
+
+
+        describe('#list', function () {
+            it('should return matching cookies for an URL', function () {
+                expect(biskviit.list('https://www.foo.com')).to.deep.equal([{
+                    name: 'ssid1',
+                    value: 'Ap4P….GTEq1',
+                    domain: '.foo.com',
+                    path: '/',
+                    httponly: true,
+                    secure: true,
+                    expires: new Date('Wed, 13 Jan 2021 22:23:01 GMT')
+                }, {
+                    name: 'ssid4',
+                    value: 'Ap4P….GTEq4',
+                    domain: 'www.foo.com',
+                    path: '/',
+                    httponly: true,
+                    secure: true,
+                    expires: new Date('Wed, 13 Jan 2021 22:23:01 GMT')
+                }]);
+            });
+        });
+
+        describe('#get', function () {
+            it('should return matching cookies for an URL', function () {
+                expect(biskviit.get('https://www.foo.com')).to.equal('ssid1=Ap4P….GTEq1; ssid4=Ap4P….GTEq4');
+            });
+        });
+    });
+
+    describe('#set', function () {
+        it('should set cookie', function () {
+            // short
+            biskviit.set('theme=plain', 'https://foo.com/');
+            // long
+            biskviit.set('SSID=Ap4P….GTEq; Domain=foo.com; Path=/test; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly', 'https://foo.com/');
+            // subdomains
+            biskviit.set('SSID=Ap4P….GTEq; Domain=.foo.com; Path=/; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly', 'https://www.foo.com/');
+            // invalid cors
+            biskviit.set('invalid_1=cors; domain=example.com', 'https://foo.com/');
+            biskviit.set('invalid_2=cors; domain=www.foo.com', 'https://foo.com/');
+            // invalid date
+            biskviit.set('invalid_3=date; Expires=zzzz', 'https://foo.com/');
+            // invalid tld
+            biskviit.set('invalid_4=cors; domain=.co.uk', 'https://foo.co.uk/');
+            // should not be added
+            biskviit.set('expired_1=date; Expires=1999-01-01 01:01:01 GMT', 'https://foo.com/');
+
+            expect(biskviit.cookies.map(function (cookie) {
+                delete cookie.expires;
+                return cookie;
+            })).to.deep.equal([{
+                name: 'theme',
+                value: 'plain',
+                domain: 'foo.com',
+                path: '/'
+            }, {
+                name: 'ssid',
+                value: 'Ap4P….GTEq',
+                domain: 'foo.com',
+                path: '/test',
+                secure: true,
+                httponly: true
+            }, {
+                name: 'ssid',
+                value: 'Ap4P….GTEq',
+                domain: 'www.foo.com',
+                path: '/',
+                secure: true,
+                httponly: true
+            }, {
+                name: 'invalid_1',
+                value: 'cors',
+                domain: 'foo.com',
+                path: '/'
+            }, {
+                name: 'invalid_2',
+                value: 'cors',
+                domain: 'foo.com',
+                path: '/'
+            }, {
+                name: 'invalid_3',
+                value: 'date',
+                domain: 'foo.com',
+                path: '/'
+            }, {
+                name: 'invalid_4',
+                value: 'cors',
+                domain: 'foo.co.uk',
+                path: '/'
+            }]);
+        });
+    });
+
+});
diff --git a/node_modules/nodemailer-fetch/test/fetch-test.js b/node_modules/nodemailer-fetch/test/fetch-test.js
new file mode 100644
index 0000000..1631e5b
--- /dev/null
+++ b/node_modules/nodemailer-fetch/test/fetch-test.js
@@ -0,0 +1,486 @@
+/* eslint no-unused-expressions:0 */
+/* globals afterEach, beforeEach, describe, it */
+
+'use strict';
+
+var chai = require('chai');
+var expect = chai.expect;
+
+//var http = require('http');
+var fetch = require('../lib/fetch');
+var http = require('http');
+var https = require('https');
+var zlib = require('zlib');
+var PassThrough = require('stream').PassThrough;
+
+chai.config.includeStack = true;
+
+var HTTP_PORT = 9998;
+var HTTPS_PORT = 9993;
+
+var httpsOptions = {
+    key: '-----BEGIN RSA PRIVATE KEY-----\n' +
+        'MIIEpAIBAAKCAQEA6Z5Qqhw+oWfhtEiMHE32Ht94mwTBpAfjt3vPpX8M7DMCTwHs\n' +
+        '1xcXvQ4lQ3rwreDTOWdoJeEEy7gMxXqH0jw0WfBx+8IIJU69xstOyT7FRFDvA1yT\n' +
+        'RXY2yt9K5s6SKken/ebMfmZR+03ND4UFsDzkz0FfgcjrkXmrMF5Eh5UXX/+9YHeU\n' +
+        'xlp0gMAt+/SumSmgCaysxZLjLpd4uXz+X+JVxsk1ACg1NoEO7lWJC/3WBP7MIcu2\n' +
+        'wVsMd2XegLT0gWYfT1/jsIH64U/mS/SVXC9QhxMl9Yfko2kx1OiYhDxhHs75RJZh\n' +
+        'rNRxgfiwgSb50Gw4NAQaDIxr/DJPdLhgnpY6UQIDAQABAoIBAE+tfzWFjJbgJ0ql\n' +
+        's6Ozs020Sh4U8TZQuonJ4HhBbNbiTtdDgNObPK1uNadeNtgW5fOeIRdKN6iDjVeN\n' +
+        'AuXhQrmqGDYVZ1HSGUfD74sTrZQvRlWPLWtzdhybK6Css41YAyPFo9k4bJ2ZW2b/\n' +
+        'p4EEQ8WsNja9oBpttMU6YYUchGxo1gujN8hmfDdXUQx3k5Xwx4KA68dveJ8GasIt\n' +
+        'd+0Jd/FVwCyyx8HTiF1FF8QZYQeAXxbXJgLBuCsMQJghlcpBEzWkscBR3Ap1U0Zi\n' +
+        '4oat8wrPZGCblaA6rNkRUVbc/+Vw0stnuJ/BLHbPxyBs6w495yBSjBqUWZMvljNz\n' +
+        'm9/aK0ECgYEA9oVIVAd0enjSVIyAZNbw11ElidzdtBkeIJdsxqhmXzeIFZbB39Gd\n' +
+        'bjtAVclVbq5mLsI1j22ER2rHA4Ygkn6vlLghK3ZMPxZa57oJtmL3oP0RvOjE4zRV\n' +
+        'dzKexNGo9gU/x9SQbuyOmuauvAYhXZxeLpv+lEfsZTqqrvPUGeBiEQcCgYEA8poG\n' +
+        'WVnykWuTmCe0bMmvYDsWpAEiZnFLDaKcSbz3O7RMGbPy1cypmqSinIYUpURBT/WY\n' +
+        'wVPAGtjkuTXtd1Cy58m7PqziB7NNWMcsMGj+lWrTPZ6hCHIBcAImKEPpd+Y9vGJX\n' +
+        'oatFJguqAGOz7rigBq6iPfeQOCWpmprNAuah++cCgYB1gcybOT59TnA7mwlsh8Qf\n' +
+        'bm+tSllnin2A3Y0dGJJLmsXEPKtHS7x2Gcot2h1d98V/TlWHe5WNEUmx1VJbYgXB\n' +
+        'pw8wj2ACxl4ojNYqWPxegaLd4DpRbtW6Tqe9e47FTnU7hIggR6QmFAWAXI+09l8y\n' +
+        'amssNShqjE9lu5YDi6BTKwKBgQCuIlKGViLfsKjrYSyHnajNWPxiUhIgGBf4PI0T\n' +
+        '/Jg1ea/aDykxv0rKHnw9/5vYGIsM2st/kR7l5mMecg/2Qa145HsLfMptHo1ZOPWF\n' +
+        '9gcuttPTegY6aqKPhGthIYX2MwSDMM+X0ri6m0q2JtqjclAjG7yG4CjbtGTt/UlE\n' +
+        'WMlSZwKBgQDslGeLUnkW0bsV5EG3AKRUyPKz/6DVNuxaIRRhOeWVKV101claqXAT\n' +
+        'wXOpdKrvkjZbT4AzcNrlGtRl3l7dEVXTu+dN7/ZieJRu7zaStlAQZkIyP9O3DdQ3\n' +
+        'rIcetQpfrJ1cAqz6Ng0pD0mh77vQ13WG1BBmDFa2A9BuzLoBituf4g==\n' +
+        '-----END RSA PRIVATE KEY-----',
+    cert: '-----BEGIN CERTIFICATE-----\n' +
+        'MIICpDCCAYwCCQCuVLVKVTXnAjANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDEwls\n' +
+        'b2NhbGhvc3QwHhcNMTUwMjEyMTEzMjU4WhcNMjUwMjA5MTEzMjU4WjAUMRIwEAYD\n' +
+        'VQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDp\n' +
+        'nlCqHD6hZ+G0SIwcTfYe33ibBMGkB+O3e8+lfwzsMwJPAezXFxe9DiVDevCt4NM5\n' +
+        'Z2gl4QTLuAzFeofSPDRZ8HH7wgglTr3Gy07JPsVEUO8DXJNFdjbK30rmzpIqR6f9\n' +
+        '5sx+ZlH7Tc0PhQWwPOTPQV+ByOuReaswXkSHlRdf/71gd5TGWnSAwC379K6ZKaAJ\n' +
+        'rKzFkuMul3i5fP5f4lXGyTUAKDU2gQ7uVYkL/dYE/swhy7bBWwx3Zd6AtPSBZh9P\n' +
+        'X+OwgfrhT+ZL9JVcL1CHEyX1h+SjaTHU6JiEPGEezvlElmGs1HGB+LCBJvnQbDg0\n' +
+        'BBoMjGv8Mk90uGCeljpRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBABXm8GPdY0sc\n' +
+        'mMUFlgDqFzcevjdGDce0QfboR+M7WDdm512Jz2SbRTgZD/4na42ThODOZz9z1AcM\n' +
+        'zLgx2ZNZzVhBz0odCU4JVhOCEks/OzSyKeGwjIb4JAY7dh+Kju1+6MNfQJ4r1Hza\n' +
+        'SVXH0+JlpJDaJ73NQ2JyfqELmJ1mTcptkA/N6rQWhlzycTBSlfogwf9xawgVPATP\n' +
+        '4AuwgjHl12JI2HVVs1gu65Y3slvaHRCr0B4+Kg1GYNLLcbFcK+NEHrHmPxy9TnTh\n' +
+        'Zwp1dsNQU+Xkylz8IUANWSLHYZOMtN2e5SKIdwTtl5C8YxveuY8YKb1gDExnMraT\n' +
+        'VGXQDqPleug=\n' +
+        '-----END CERTIFICATE-----'
+};
+
+describe('fetch tests', function () {
+    var httpServer, httpsServer;
+
+    beforeEach(function (done) {
+        httpServer = http.createServer(function (req, res) {
+            switch (req.url) {
+
+                case '/redirect6':
+                    res.writeHead(302, {
+                        Location: '/redirect5'
+                    });
+                    res.end();
+                    break;
+
+                case '/redirect5':
+                    res.writeHead(302, {
+                        Location: '/redirect4'
+                    });
+                    res.end();
+                    break;
+
+                case '/redirect4':
+                    res.writeHead(302, {
+                        Location: '/redirect3'
+                    });
+                    res.end();
+                    break;
+
+                case '/redirect3':
+                    res.writeHead(302, {
+                        Location: '/redirect2'
+                    });
+                    res.end();
+                    break;
+
+                case '/redirect2':
+                    res.writeHead(302, {
+                        Location: '/redirect1'
+                    });
+                    res.end();
+                    break;
+
+                case '/redirect1':
+                    res.writeHead(302, {
+                        Location: '/'
+                    });
+                    res.end();
+                    break;
+
+                case '/forever':
+                    res.writeHead(200, {
+                        'Content-Type': 'text/plain'
+                    });
+                    res.write('This connection is never closed');
+                    // never end the request
+                    break;
+
+                case '/gzip':
+                    res.writeHead(200, {
+                        'Content-Type': 'text/plain',
+                        'Content-Encoding': 'gzip'
+                    });
+
+                    var stream = zlib.createGzip();
+                    stream.pipe(res);
+                    stream.end('Hello World HTTP\n');
+                    break;
+
+                case '/invalid':
+                    res.writeHead(500, {
+                        'Content-Type': 'text/plain'
+                    });
+                    res.end('Hello World HTTP\n');
+                    break;
+
+                case '/auth':
+                    res.writeHead(200, {
+                        'Content-Type': 'text/plain'
+                    });
+                    res.end(new Buffer(req.headers.authorization.split(' ').pop(), 'base64'));
+                    break;
+
+                case '/cookie':
+                    res.writeHead(200, {
+                        'Content-Type': 'text/plain'
+                    });
+                    res.end(req.headers.cookie);
+                    break;
+
+                case '/ua':
+                    res.writeHead(200, {
+                        'Content-Type': 'text/plain'
+                    });
+                    res.end(req.headers['user-agent']);
+                    break;
+
+                case '/post':
+                    var body = [];
+                    req.on('readable', function () {
+                        var chunk;
+                        while ((chunk = req.read()) !== null) {
+                            body.push(chunk);
+                        }
+                    });
+                    req.on('end', function () {
+                        res.writeHead(200, {
+                            'Content-Type': 'text/plain'
+                        });
+                        res.end(Buffer.concat(body));
+                    });
+
+                    break;
+
+                default:
+                    res.writeHead(200, {
+                        'Content-Type': 'text/plain'
+                    });
+                    res.end('Hello World HTTP\n');
+            }
+        });
+
+        httpsServer = https.createServer(httpsOptions, function (req, res) {
+            res.writeHead(200, {
+                'Content-Type': 'text/plain'
+            });
+            res.end('Hello World HTTPS\n');
+        });
+
+        httpServer.listen(HTTP_PORT, function () {
+            httpsServer.listen(HTTPS_PORT, done);
+        });
+    });
+
+    afterEach(function (done) {
+        httpServer.close(function () {
+            httpsServer.close(done);
+        });
+    });
+
+    it('should fetch HTTP data', function (done) {
+        var req = fetch('http://localhost:' + HTTP_PORT);
+        var buf = [];
+        req.on('data', function (chunk) {
+            buf.push(chunk);
+        });
+        req.on('end', function () {
+            expect(Buffer.concat(buf).toString()).to.equal('Hello World HTTP\n');
+            done();
+        });
+    });
+
+    it('should fetch HTTPS data', function (done) {
+        var req = fetch('https://localhost:' + HTTPS_PORT);
+        var buf = [];
+        req.on('data', function (chunk) {
+            buf.push(chunk);
+        });
+        req.on('end', function () {
+            expect(Buffer.concat(buf).toString()).to.equal('Hello World HTTPS\n');
+            done();
+        });
+    });
+
+    it('should fetch HTTP data with redirects', function (done) {
+        var req = fetch('http://localhost:' + HTTP_PORT + '/redirect3');
+        var buf = [];
+        req.on('data', function (chunk) {
+            buf.push(chunk);
+        });
+        req.on('end', function () {
+            expect(Buffer.concat(buf).toString()).to.equal('Hello World HTTP\n');
+            done();
+        });
+    });
+
+    it('should return error for too many redirects', function (done) {
+        var req = fetch('http://localhost:' + HTTP_PORT + '/redirect6');
+        var buf = [];
+        req.on('data', function (chunk) {
+            buf.push(chunk);
+        });
+        req.on('error', function (err) {
+            expect(err).to.exist;
+            done();
+        });
+        req.on('end', function () {});
+    });
+
+    it('should fetch HTTP data with custom redirect limit', function (done) {
+        var req = fetch('http://localhost:' + HTTP_PORT + '/redirect3', {
+            maxRedirects: 3
+        });
+        var buf = [];
+        req.on('data', function (chunk) {
+            buf.push(chunk);
+        });
+        req.on('end', function () {
+            expect(Buffer.concat(buf).toString()).to.equal('Hello World HTTP\n');
+            done();
+        });
+    });
+
+    it('should return error for custom redirect limit', function (done) {
+        var req = fetch('http://localhost:' + HTTP_PORT + '/redirect3', {
+            maxRedirects: 2
+        });
+        var buf = [];
+        req.on('data', function (chunk) {
+            buf.push(chunk);
+        });
+        req.on('error', function (err) {
+            expect(err).to.exist;
+            done();
+        });
+        req.on('end', function () {});
+    });
+
+    it('should return disable redirects', function (done) {
+        var req = fetch('http://localhost:' + HTTP_PORT + '/redirect1', {
+            maxRedirects: 0
+        });
+        var buf = [];
+        req.on('data', function (chunk) {
+            buf.push(chunk);
+        });
+        req.on('error', function (err) {
+            expect(err).to.exist;
+            done();
+        });
+        req.on('end', function () {});
+    });
+
+    it('should unzip compressed HTTP data', function (done) {
+        var req = fetch('http://localhost:' + HTTP_PORT + '/gzip');
+        var buf = [];
+        req.on('data', function (chunk) {
+            buf.push(chunk);
+        });
+        req.on('end', function () {
+            expect(Buffer.concat(buf).toString()).to.equal('Hello World HTTP\n');
+            done();
+        });
+    });
+
+    it('should return error for unresolved host', function (done) {
+        var req = fetch('http://asfhaskhhgbjdsfhgbsdjgk');
+        var buf = [];
+        req.on('data', function (chunk) {
+            buf.push(chunk);
+        });
+        req.on('error', function (err) {
+            expect(err).to.exist;
+            done();
+        });
+        req.on('end', function () {});
+    });
+
+    it('should return error for invalid status', function (done) {
+        var req = fetch('http://localhost:' + HTTP_PORT + '/invalid');
+        var buf = [];
+        req.on('data', function (chunk) {
+            buf.push(chunk);
+        });
+        req.on('error', function (err) {
+            expect(err).to.exist;
+            done();
+        });
+        req.on('end', function () {});
+    });
+
+    it('should return error for invalid url', function (done) {
+        var req = fetch('http://localhost:99999999/');
+        var buf = [];
+        req.on('data', function (chunk) {
+            buf.push(chunk);
+        });
+        req.on('error', function (err) {
+            expect(err).to.exist;
+            done();
+        });
+        req.on('end', function () {});
+    });
+
+    it('should return timeout error', function (done) {
+        var req = fetch('http://localhost:' + HTTP_PORT + '/forever', {
+            timeout: 1000
+        });
+        var buf = [];
+        req.on('data', function (chunk) {
+            buf.push(chunk);
+        });
+        req.on('error', function (err) {
+            expect(err).to.exist;
+            done();
+        });
+        req.on('end', function () {});
+    });
+
+    it('should handle basic HTTP auth', function (done) {
+        var req = fetch('http://user:pass@localhost:' + HTTP_PORT + '/auth');
+        var buf = [];
+        req.on('data', function (chunk) {
+            buf.push(chunk);
+        });
+        req.on('end', function () {
+            expect(Buffer.concat(buf).toString()).to.equal('user:pass');
+            done();
+        });
+    });
+
+    if (!/^0\.10\./.test(process.versions.node)) {
+        // disabled for node 0.10
+        it('should return error for invalid protocol', function (done) {
+            var req = fetch('http://localhost:' + HTTPS_PORT);
+            var buf = [];
+            req.on('data', function (chunk) {
+                buf.push(chunk);
+            });
+            req.on('error', function (err) {
+                expect(err).to.exist;
+                done();
+            });
+            req.on('end', function () {});
+        });
+    }
+
+    it('should set cookie value', function (done) {
+        var req = fetch('http://localhost:' + HTTP_PORT + '/cookie', {
+            cookie: 'test=pest'
+        });
+        var buf = [];
+        req.on('data', function (chunk) {
+            buf.push(chunk);
+        });
+        req.on('end', function () {
+            expect(Buffer.concat(buf).toString()).to.equal('test=pest');
+            done();
+        });
+    });
+
+    it('should set user agent', function (done) {
+        var req = fetch('http://localhost:' + HTTP_PORT + '/ua', {
+            userAgent: 'nodemailer-fetch'
+        });
+        var buf = [];
+        req.on('data', function (chunk) {
+            buf.push(chunk);
+        });
+        req.on('end', function () {
+            expect(Buffer.concat(buf).toString()).to.equal('nodemailer-fetch');
+            done();
+        });
+    });
+
+    it('should post data', function (done) {
+        var req = fetch('http://localhost:' + HTTP_PORT + '/post', {
+            method: 'post',
+            body: {
+                hello: 'world 😭',
+                another: 'value'
+            }
+        });
+        var buf = [];
+        req.on('data', function (chunk) {
+            buf.push(chunk);
+        });
+        req.on('end', function () {
+            expect(Buffer.concat(buf).toString()).to.equal('hello=world%20%F0%9F%98%AD&another=value');
+            done();
+        });
+    });
+
+    it('should post stream data', function (done) {
+        var body = new PassThrough();
+        var data = new Buffer('hello=world%20%F0%9F%98%AD&another=value');
+
+        var req = fetch('http://localhost:' + HTTP_PORT + '/post', {
+            method: 'post',
+            body: body
+        });
+        var buf = [];
+        req.on('data', function (chunk) {
+            buf.push(chunk);
+        });
+        req.on('end', function () {
+            expect(Buffer.concat(buf).toString()).to.equal(data.toString());
+            done();
+        });
+
+        var pos = 0;
+        var writeNext = function () {
+            if (pos >= data.length) {
+                return body.end();
+            }
+            var char = data.slice(pos++, pos);
+            body.write(char);
+            setImmediate(writeNext);
+        };
+
+        setImmediate(writeNext);
+    });
+
+    it('should return error for invalid cert', function (done) {
+        var req = fetch('https://localhost:' + HTTPS_PORT, {
+            tls: {
+                rejectUnauthorized: true
+            }
+        });
+        var buf = [];
+        req.on('data', function (chunk) {
+            buf.push(chunk);
+        });
+        req.on('error', function (err) {
+            expect(err).to.exist;
+            done();
+        });
+        req.on('end', function () {});
+    });
+});
diff --git a/node_modules/nodemailer-shared/.eslintrc.js b/node_modules/nodemailer-shared/.eslintrc.js
new file mode 100644
index 0000000..0dca0ee
--- /dev/null
+++ b/node_modules/nodemailer-shared/.eslintrc.js
@@ -0,0 +1,59 @@
+'use strict';
+
+module.exports = {
+    rules: {
+        indent: [2, 4, {
+            SwitchCase: 1
+        }],
+        quotes: [2, 'single'],
+        'linebreak-style': [2, 'unix'],
+        semi: [2, 'always'],
+        strict: [2, 'global'],
+        eqeqeq: 2,
+        'dot-notation': 2,
+        curly: 2,
+        'no-fallthrough': 2,
+        'quote-props': [2, 'as-needed'],
+        'no-unused-expressions': [2, {
+            allowShortCircuit: true
+        }],
+        'no-unused-vars': 2,
+        'no-undef': 2,
+        'handle-callback-err': 2,
+        'no-new': 2,
+        'new-cap': 2,
+        'no-eval': 2,
+        'no-invalid-this': 2,
+        radix: [2, 'always'],
+        'no-use-before-define': [2, 'nofunc'],
+        'callback-return': [2, ['callback', 'cb', 'done']],
+        'comma-dangle': [2, 'never'],
+        'comma-style': [2, 'last'],
+        'no-regex-spaces': 2,
+        'no-empty': 2,
+        'no-duplicate-case': 2,
+        'no-empty-character-class': 2,
+        'no-redeclare': [2, {
+            builtinGlobals: true
+        }],
+        'block-scoped-var': 2,
+        'no-sequences': 2,
+        'no-throw-literal': 2,
+        'no-useless-concat': 2,
+        'no-void': 2,
+        yoda: 2,
+        'no-bitwise': 2,
+        'no-lonely-if': 2,
+        'no-mixed-spaces-and-tabs': 2,
+        'no-console': 0
+    },
+    env: {
+        es6: false,
+        node: true
+    },
+    extends: 'eslint:recommended',
+    fix: true,
+    globals: {
+        Promise: false
+    }
+};
diff --git a/node_modules/nodemailer-shared/.npmignore b/node_modules/nodemailer-shared/.npmignore
new file mode 100644
index 0000000..2f6141d
--- /dev/null
+++ b/node_modules/nodemailer-shared/.npmignore
@@ -0,0 +1,3 @@
+node_modules
+npm-debug.log
+.DS_Store
\ No newline at end of file
diff --git a/node_modules/nodemailer-shared/.travis.yml b/node_modules/nodemailer-shared/.travis.yml
new file mode 100644
index 0000000..61fe757
--- /dev/null
+++ b/node_modules/nodemailer-shared/.travis.yml
@@ -0,0 +1,18 @@
+language: node_js
+sudo: false
+node_js:
+  - 0.12
+  - iojs
+  - '4'
+  - '6'
+before_install:
+  - npm install -g grunt-cli
+notifications:
+  email:
+    - andris@kreata.ee
+  webhooks:
+    urls:
+      - https://webhooks.gitter.im/e/0ed18fd9b3e529b3c2cc
+    on_success: change  # options: [always|never|change] default: always
+    on_failure: always  # options: [always|never|change] default: always
+    on_start: false     # default: false
diff --git a/node_modules/nodemailer-shared/Gruntfile.js b/node_modules/nodemailer-shared/Gruntfile.js
new file mode 100644
index 0000000..77e262b
--- /dev/null
+++ b/node_modules/nodemailer-shared/Gruntfile.js
@@ -0,0 +1,27 @@
+'use strict';
+
+module.exports = function (grunt) {
+
+    // Project configuration.
+    grunt.initConfig({
+        eslint: {
+            all: ['lib/*.js', 'test/*.js', 'Gruntfile.js']
+        },
+
+        mochaTest: {
+            all: {
+                options: {
+                    reporter: 'spec'
+                },
+                src: ['test/*-test.js']
+            }
+        }
+    });
+
+    // Load the plugin(s)
+    grunt.loadNpmTasks('grunt-eslint');
+    grunt.loadNpmTasks('grunt-mocha-test');
+
+    // Tasks
+    grunt.registerTask('default', ['eslint', 'mochaTest']);
+};
diff --git a/node_modules/nodemailer-shared/LICENSE b/node_modules/nodemailer-shared/LICENSE
new file mode 100644
index 0000000..a22c7ce
--- /dev/null
+++ b/node_modules/nodemailer-shared/LICENSE
@@ -0,0 +1,16 @@
+Copyright (c) 2016 Andris Reinman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/node_modules/nodemailer-shared/README.md b/node_modules/nodemailer-shared/README.md
new file mode 100644
index 0000000..c26cd93
--- /dev/null
+++ b/node_modules/nodemailer-shared/README.md
@@ -0,0 +1,14 @@
+# nodemailer-shared
+
+Shared methods for the [Nodemailer](https://github.com/nodemailer/nodemailer) stack.
+
+## Methods
+
+  * `parseConnectionUrl(str)` parses a connection url into a nodemailer configuration object
+  * `getLogger(options)` returns a bunyan compatible logger instance
+  * `callbackPromise(resolve, reject)` returns a promise-resolving function suitable for using as a callback
+  * `resolveContent(data, key, callback)` converts a key of a data object from stream/url/path to a buffer
+
+## License
+
+**MIT**
diff --git a/node_modules/nodemailer-shared/lib/shared.js b/node_modules/nodemailer-shared/lib/shared.js
new file mode 100644
index 0000000..c6a98aa
--- /dev/null
+++ b/node_modules/nodemailer-shared/lib/shared.js
@@ -0,0 +1,282 @@
+'use strict';
+
+var urllib = require('url');
+var util = require('util');
+var fs = require('fs');
+var fetch = require('nodemailer-fetch');
+
+/**
+ * Parses connection url to a structured configuration object
+ *
+ * @param {String} str Connection url
+ * @return {Object} Configuration object
+ */
+module.exports.parseConnectionUrl = function (str) {
+    str = str || '';
+    var options = {};
+
+    [urllib.parse(str, true)].forEach(function (url) {
+        var auth;
+
+        switch (url.protocol) {
+            case 'smtp:':
+                options.secure = false;
+                break;
+            case 'smtps:':
+                options.secure = true;
+                break;
+            case 'direct:':
+                options.direct = true;
+                break;
+        }
+
+        if (!isNaN(url.port) && Number(url.port)) {
+            options.port = Number(url.port);
+        }
+
+        if (url.hostname) {
+            options.host = url.hostname;
+        }
+
+        if (url.auth) {
+            auth = url.auth.split(':');
+
+            if (!options.auth) {
+                options.auth = {};
+            }
+
+            options.auth.user = auth.shift();
+            options.auth.pass = auth.join(':');
+        }
+
+        Object.keys(url.query || {}).forEach(function (key) {
+            var obj = options;
+            var lKey = key;
+            var value = url.query[key];
+
+            if (!isNaN(value)) {
+                value = Number(value);
+            }
+
+            switch (value) {
+                case 'true':
+                    value = true;
+                    break;
+                case 'false':
+                    value = false;
+                    break;
+            }
+
+            // tls is nested object
+            if (key.indexOf('tls.') === 0) {
+                lKey = key.substr(4);
+                if (!options.tls) {
+                    options.tls = {};
+                }
+                obj = options.tls;
+            } else if (key.indexOf('.') >= 0) {
+                // ignore nested properties besides tls
+                return;
+            }
+
+            if (!(lKey in obj)) {
+                obj[lKey] = value;
+            }
+        });
+    });
+
+    return options;
+};
+
+/**
+ * Returns a bunyan-compatible logger interface. Uses either provided logger or
+ * creates a default console logger
+ *
+ * @param {Object} [options] Options object that might include 'logger' value
+ * @return {Object} bunyan compatible logger
+ */
+module.exports.getLogger = function (options) {
+    options = options || {};
+
+    if (!options.logger) {
+        // use vanity logger
+        return {
+            info: function () {},
+            debug: function () {},
+            error: function () {}
+        };
+    }
+
+    if (options.logger === true) {
+        // create console logger
+        return createDefaultLogger();
+    }
+
+    // return whatever was passed
+    return options.logger;
+};
+
+/**
+ * Wrapper for creating a callback than either resolves or rejects a promise
+ * based on input
+ *
+ * @param {Function} resolve Function to run if callback is called
+ * @param {Function} reject Function to run if callback ends with an error
+ */
+module.exports.callbackPromise = function (resolve, reject) {
+    return function () {
+        var args = Array.prototype.slice.call(arguments);
+        var err = args.shift();
+        if (err) {
+            reject(err);
+        } else {
+            resolve.apply(null, args);
+        }
+    };
+};
+
+/**
+ * Resolves a String or a Buffer value for content value. Useful if the value
+ * is a Stream or a file or an URL. If the value is a Stream, overwrites
+ * the stream object with the resolved value (you can't stream a value twice).
+ *
+ * This is useful when you want to create a plugin that needs a content value,
+ * for example the `html` or `text` value as a String or a Buffer but not as
+ * a file path or an URL.
+ *
+ * @param {Object} data An object or an Array you want to resolve an element for
+ * @param {String|Number} key Property name or an Array index
+ * @param {Function} callback Callback function with (err, value)
+ */
+module.exports.resolveContent = function (data, key, callback) {
+    var promise;
+
+    if (!callback && typeof Promise === 'function') {
+        promise = new Promise(function (resolve, reject) {
+            callback = module.exports.callbackPromise(resolve, reject);
+        });
+    }
+
+    var content = data && data[key] && data[key].content || data[key];
+    var contentStream;
+    var encoding = (typeof data[key] === 'object' && data[key].encoding || 'utf8')
+        .toString()
+        .toLowerCase()
+        .replace(/[-_\s]/g, '');
+
+    if (!content) {
+        return callback(null, content);
+    }
+
+    if (typeof content === 'object') {
+        if (typeof content.pipe === 'function') {
+            return resolveStream(content, function (err, value) {
+                if (err) {
+                    return callback(err);
+                }
+                // we can't stream twice the same content, so we need
+                // to replace the stream object with the streaming result
+                data[key] = value;
+                callback(null, value);
+            });
+        } else if (/^https?:\/\//i.test(content.path || content.href)) {
+            contentStream = fetch(content.path || content.href);
+            return resolveStream(contentStream, callback);
+        } else if (/^data:/i.test(content.path || content.href)) {
+            var parts = (content.path || content.href).match(/^data:((?:[^;]*;)*(?:[^,]*)),(.*)$/i);
+            if (!parts) {
+                return callback(null, new Buffer(0));
+            }
+            return callback(null, /\bbase64$/i.test(parts[1]) ? new Buffer(parts[2], 'base64') : new Buffer(decodeURIComponent(parts[2])));
+        } else if (content.path) {
+            return resolveStream(fs.createReadStream(content.path), callback);
+        }
+    }
+
+    if (typeof data[key].content === 'string' && ['utf8', 'usascii', 'ascii'].indexOf(encoding) < 0) {
+        content = new Buffer(data[key].content, encoding);
+    }
+
+    // default action, return as is
+    setImmediate(callback.bind(null, null, content));
+
+    return promise;
+};
+
+/**
+ * Streams a stream value into a Buffer
+ *
+ * @param {Object} stream Readable stream
+ * @param {Function} callback Callback function with (err, value)
+ */
+function resolveStream(stream, callback) {
+    var responded = false;
+    var chunks = [];
+    var chunklen = 0;
+
+    stream.on('error', function (err) {
+        if (responded) {
+            return;
+        }
+
+        responded = true;
+        callback(err);
+    });
+
+    stream.on('readable', function () {
+        var chunk;
+        while ((chunk = stream.read()) !== null) {
+            chunks.push(chunk);
+            chunklen += chunk.length;
+        }
+    });
+
+    stream.on('end', function () {
+        if (responded) {
+            return;
+        }
+        responded = true;
+
+        var value;
+
+        try {
+            value = Buffer.concat(chunks, chunklen);
+        } catch (E) {
+            return callback(E);
+        }
+        callback(null, value);
+    });
+}
+
+/**
+ * Generates a bunyan-like logger that prints to console
+ *
+ * @returns {Object} Bunyan logger instance
+ */
+function createDefaultLogger() {
+
+    var logger = {
+        _print: function ( /* level, message */ ) {
+            var args = Array.prototype.slice.call(arguments);
+            var level = args.shift();
+            var message;
+
+            if (args.length > 1) {
+                message = util.format.apply(util, args);
+            } else {
+                message = args.shift();
+            }
+
+            console.log('[%s] %s: %s',
+                new Date().toISOString().substr(0, 19).replace(/T/, ' '),
+                level.toUpperCase(),
+                message);
+        }
+    };
+
+    logger.info = logger._print.bind(null, 'info');
+    logger.debug = logger._print.bind(null, 'debug');
+    logger.error = logger._print.bind(null, 'error');
+
+    return logger;
+}
diff --git a/node_modules/nodemailer-shared/package.json b/node_modules/nodemailer-shared/package.json
new file mode 100644
index 0000000..c98b8b7
--- /dev/null
+++ b/node_modules/nodemailer-shared/package.json
@@ -0,0 +1,99 @@
+{
+  "_args": [
+    [
+      {
+        "raw": "nodemailer-shared@1.1.0",
+        "scope": null,
+        "escapedName": "nodemailer-shared",
+        "name": "nodemailer-shared",
+        "rawSpec": "1.1.0",
+        "spec": "1.1.0",
+        "type": "version"
+      },
+      "/Users/fzy/project/koa2_Sequelize_project/node_modules/nodemailer-smtp-transport"
+    ]
+  ],
+  "_from": "nodemailer-shared@1.1.0",
+  "_id": "nodemailer-shared@1.1.0",
+  "_inCache": true,
+  "_location": "/nodemailer-shared",
+  "_nodeVersion": "6.5.0",
+  "_npmOperationalInternal": {
+    "host": "packages-16-east.internal.npmjs.com",
+    "tmp": "tmp/nodemailer-shared-1.1.0.tgz_1473077999789_0.5456023884471506"
+  },
+  "_npmUser": {
+    "name": "andris",
+    "email": "andris@kreata.ee"
+  },
+  "_npmVersion": "3.10.3",
+  "_phantomChildren": {},
+  "_requested": {
+    "raw": "nodemailer-shared@1.1.0",
+    "scope": null,
+    "escapedName": "nodemailer-shared",
+    "name": "nodemailer-shared",
+    "rawSpec": "1.1.0",
+    "spec": "1.1.0",
+    "type": "version"
+  },
+  "_requiredBy": [
+    "/nodemailer-smtp-transport",
+    "/smtp-connection"
+  ],
+  "_resolved": "https://registry.npmjs.org/nodemailer-shared/-/nodemailer-shared-1.1.0.tgz",
+  "_shasum": "cf5994e2fd268d00f5cf0fa767a08169edb07ec0",
+  "_shrinkwrap": null,
+  "_spec": "nodemailer-shared@1.1.0",
+  "_where": "/Users/fzy/project/koa2_Sequelize_project/node_modules/nodemailer-smtp-transport",
+  "author": {
+    "name": "Andris Reinman"
+  },
+  "bugs": {
+    "url": "https://github.com/nodemailer/nodemailer-shared/issues"
+  },
+  "dependencies": {
+    "nodemailer-fetch": "1.6.0"
+  },
+  "description": "Shared methods for the nodemailer stack",
+  "devDependencies": {
+    "chai": "^3.5.0",
+    "grunt": "^1.0.1",
+    "grunt-cli": "^1.2.0",
+    "grunt-eslint": "^19.0.0",
+    "grunt-mocha-test": "^0.12.7",
+    "mocha": "^3.0.2"
+  },
+  "directories": {
+    "test": "test"
+  },
+  "dist": {
+    "shasum": "cf5994e2fd268d00f5cf0fa767a08169edb07ec0",
+    "tarball": "https://registry.npmjs.org/nodemailer-shared/-/nodemailer-shared-1.1.0.tgz"
+  },
+  "gitHead": "b784fed89fee789766a32fcdc74192bc5373565c",
+  "homepage": "https://github.com/nodemailer/nodemailer-shared#readme",
+  "keywords": [
+    "nodemailer"
+  ],
+  "license": "MIT",
+  "main": "lib/shared.js",
+  "maintainers": [
+    {
+      "name": "andris",
+      "email": "andris@kreata.ee"
+    }
+  ],
+  "name": "nodemailer-shared",
+  "optionalDependencies": {},
+  "readme": "# nodemailer-shared\n\nShared methods for the [Nodemailer](https://github.com/nodemailer/nodemailer) stack.\n\n## Methods\n\n  * `parseConnectionUrl(str)` parses a connection url into a nodemailer configuration object\n  * `getLogger(options)` returns a bunyan compatible logger instance\n  * `callbackPromise(resolve, reject)` returns a promise-resolving function suitable for using as a callback\n  * `resolveContent(data, key, callback)` converts a key of a data object from stream/url/path to a buffer\n\n## License\n\n**MIT**\n",
+  "readmeFilename": "README.md",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/nodemailer/nodemailer-shared.git"
+  },
+  "scripts": {
+    "test": "grunt mochaTest"
+  },
+  "version": "1.1.0"
+}
diff --git a/node_modules/nodemailer-shared/test/fixtures/message.html b/node_modules/nodemailer-shared/test/fixtures/message.html
new file mode 100644
index 0000000..ca6b752
--- /dev/null
+++ b/node_modules/nodemailer-shared/test/fixtures/message.html
@@ -0,0 +1 @@
+<p>Tere, tere</p><p>vana kere!</p>
diff --git a/node_modules/nodemailer-shared/test/shared-test.js b/node_modules/nodemailer-shared/test/shared-test.js
new file mode 100644
index 0000000..906010f
--- /dev/null
+++ b/node_modules/nodemailer-shared/test/shared-test.js
@@ -0,0 +1,291 @@
+/* eslint no-unused-expressions:0, no-invalid-this:0 */
+/* globals beforeEach, afterEach, describe, it */
+
+'use strict';
+
+var chai = require('chai');
+var expect = chai.expect;
+var shared = require('../lib/shared');
+
+var http = require('http');
+var fs = require('fs');
+var zlib = require('zlib');
+
+chai.config.includeStack = true;
+
+describe('Logger tests', function () {
+    it('Should create a logger', function () {
+        expect(typeof shared.getLogger({
+            logger: false
+        })).to.equal('object');
+        expect(typeof shared.getLogger()).to.equal('object');
+        expect(typeof shared.getLogger({
+            logger: 'stri'
+        })).to.equal('string');
+    });
+});
+
+describe('Connection url parser tests', function () {
+    it('Should parse connection url', function () {
+        var url = 'smtps://user:pass@localhost:123?tls.rejectUnauthorized=false&name=horizon';
+        expect(shared.parseConnectionUrl(url)).to.deep.equal({
+            secure: true,
+            port: 123,
+            host: 'localhost',
+            auth: {
+                user: 'user',
+                pass: 'pass'
+            },
+            tls: {
+                rejectUnauthorized: false
+            },
+            name: 'horizon'
+        });
+    });
+
+    it('should not choke on special symbols in auth', function () {
+        var url = 'smtps://user%40gmail.com:%3Apasswith%25Char@smtp.gmail.com';
+        expect(shared.parseConnectionUrl(url)).to.deep.equal({
+            secure: true,
+            host: 'smtp.gmail.com',
+            auth: {
+                user: 'user@gmail.com',
+                pass: ':passwith%Char'
+            }
+        });
+    });
+});
+
+describe('Resolver tests', function () {
+    var port = 10337;
+    var server;
+
+    beforeEach(function (done) {
+        server = http.createServer(function (req, res) {
+            if (/redirect/.test(req.url)) {
+                res.writeHead(302, {
+                    Location: 'http://localhost:' + port + '/message.html'
+                });
+                res.end('Go to http://localhost:' + port + '/message.html');
+            } else if (/compressed/.test(req.url)) {
+                res.writeHead(200, {
+                    'Content-Type': 'text/plain',
+                    'Content-Encoding': 'gzip'
+                });
+                var stream = zlib.createGzip();
+                stream.pipe(res);
+                stream.write('<p>Tere, tere</p><p>vana kere!</p>\n');
+                stream.end();
+            } else {
+                res.writeHead(200, {
+                    'Content-Type': 'text/plain'
+                });
+                res.end('<p>Tere, tere</p><p>vana kere!</p>\n');
+            }
+        });
+
+        server.listen(port, done);
+    });
+
+    afterEach(function (done) {
+        server.close(done);
+    });
+
+    it('should set text from html string', function (done) {
+        var mail = {
+            data: {
+                html: '<p>Tere, tere</p><p>vana kere!</p>\n'
+            }
+        };
+        shared.resolveContent(mail.data, 'html', function (err, value) {
+            expect(err).to.not.exist;
+            expect(value).to.equal('<p>Tere, tere</p><p>vana kere!</p>\n');
+            done();
+        });
+    });
+
+    it('should set text from html buffer', function (done) {
+        var mail = {
+            data: {
+                html: new Buffer('<p>Tere, tere</p><p>vana kere!</p>\n')
+            }
+        };
+        shared.resolveContent(mail.data, 'html', function (err, value) {
+            expect(err).to.not.exist;
+            expect(value).to.deep.equal(mail.data.html);
+            done();
+        });
+    });
+
+    it('should set text from a html file', function (done) {
+        var mail = {
+            data: {
+                html: {
+                    path: __dirname + '/fixtures/message.html'
+                }
+            }
+        };
+        shared.resolveContent(mail.data, 'html', function (err, value) {
+            expect(err).to.not.exist;
+            expect(value).to.deep.equal(new Buffer('<p>Tere, tere</p><p>vana kere!</p>\n'));
+            done();
+        });
+    });
+
+    it('should set text from an html url', function (done) {
+        var mail = {
+            data: {
+                html: {
+                    path: 'http://localhost:' + port + '/message.html'
+                }
+            }
+        };
+        shared.resolveContent(mail.data, 'html', function (err, value) {
+            expect(err).to.not.exist;
+            expect(value).to.deep.equal(new Buffer('<p>Tere, tere</p><p>vana kere!</p>\n'));
+            done();
+        });
+    });
+
+    it('should set text from redirecting url', function (done) {
+        var mail = {
+            data: {
+                html: {
+                    path: 'http://localhost:' + port + '/redirect.html'
+                }
+            }
+        };
+        shared.resolveContent(mail.data, 'html', function (err, value) {
+            expect(err).to.not.exist;
+            expect(value).to.deep.equal(new Buffer('<p>Tere, tere</p><p>vana kere!</p>\n'));
+            done();
+        });
+    });
+
+    it('should set text from gzipped url', function (done) {
+        var mail = {
+            data: {
+                html: {
+                    path: 'http://localhost:' + port + '/compressed.html'
+                }
+            }
+        };
+        shared.resolveContent(mail.data, 'html', function (err, value) {
+            expect(err).to.not.exist;
+            expect(value).to.deep.equal(new Buffer('<p>Tere, tere</p><p>vana kere!</p>\n'));
+            done();
+        });
+    });
+
+    it('should set text from a html stream', function (done) {
+        var mail = {
+            data: {
+                html: fs.createReadStream(__dirname + '/fixtures/message.html')
+            }
+        };
+        shared.resolveContent(mail.data, 'html', function (err, value) {
+            expect(err).to.not.exist;
+            expect(mail).to.deep.equal({
+                data: {
+                    html: new Buffer('<p>Tere, tere</p><p>vana kere!</p>\n')
+                }
+            });
+            expect(value).to.deep.equal(new Buffer('<p>Tere, tere</p><p>vana kere!</p>\n'));
+            done();
+        });
+    });
+
+    it('should return an error', function (done) {
+        var mail = {
+            data: {
+                html: {
+                    path: 'http://localhost:' + (port + 1000) + '/message.html'
+                }
+            }
+        };
+        shared.resolveContent(mail.data, 'html', function (err) {
+            expect(err).to.exist;
+            done();
+        });
+    });
+
+    it('should return encoded string as buffer', function (done) {
+        var str = '<p>Tere, tere</p><p>vana kere!</p>\n';
+        var mail = {
+            data: {
+                html: {
+                    encoding: 'base64',
+                    content: new Buffer(str).toString('base64')
+                }
+            }
+        };
+        shared.resolveContent(mail.data, 'html', function (err, value) {
+            expect(err).to.not.exist;
+            expect(value).to.deep.equal(new Buffer(str));
+            done();
+        });
+    });
+
+    describe('data uri tests', function () {
+
+        it('should resolve with mime type and base64', function (done) {
+            var mail = {
+                data: {
+                    attachment: {
+                        path: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='
+                    }
+                }
+            };
+            shared.resolveContent(mail.data, 'attachment', function (err, value) {
+                expect(err).to.not.exist;
+                expect(value).to.deep.equal(new Buffer('iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==', 'base64'));
+                done();
+            });
+        });
+
+        it('should resolve with mime type and plaintext', function (done) {
+            var mail = {
+                data: {
+                    attachment: {
+                        path: 'data:image/png,tere%20tere'
+                    }
+                }
+            };
+            shared.resolveContent(mail.data, 'attachment', function (err, value) {
+                expect(err).to.not.exist;
+                expect(value).to.deep.equal(new Buffer('tere tere'));
+                done();
+            });
+        });
+
+        it('should resolve with plaintext', function (done) {
+            var mail = {
+                data: {
+                    attachment: {
+                        path: 'data:,tere%20tere'
+                    }
+                }
+            };
+            shared.resolveContent(mail.data, 'attachment', function (err, value) {
+                expect(err).to.not.exist;
+                expect(value).to.deep.equal(new Buffer('tere tere'));
+                done();
+            });
+        });
+
+        it('should resolve with mime type, charset and base64', function (done) {
+            var mail = {
+                data: {
+                    attachment: {
+                        path: 'data:image/png;charset=iso-8859-1;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='
+                    }
+                }
+            };
+            shared.resolveContent(mail.data, 'attachment', function (err, value) {
+                expect(err).to.not.exist;
+                expect(value).to.deep.equal(new Buffer('iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==', 'base64'));
+                done();
+            });
+        });
+    });
+});
diff --git a/node_modules/nodemailer-smtp-transport/.eslintrc.js b/node_modules/nodemailer-smtp-transport/.eslintrc.js
new file mode 100644
index 0000000..0dca0ee
--- /dev/null
+++ b/node_modules/nodemailer-smtp-transport/.eslintrc.js
@@ -0,0 +1,59 @@
+'use strict';
+
+module.exports = {
+    rules: {
+        indent: [2, 4, {
+            SwitchCase: 1
+        }],
+        quotes: [2, 'single'],
+        'linebreak-style': [2, 'unix'],
+        semi: [2, 'always'],
+        strict: [2, 'global'],
+        eqeqeq: 2,
+        'dot-notation': 2,
+        curly: 2,
+        'no-fallthrough': 2,
+        'quote-props': [2, 'as-needed'],
+        'no-unused-expressions': [2, {
+            allowShortCircuit: true
+        }],
+        'no-unused-vars': 2,
+        'no-undef': 2,
+        'handle-callback-err': 2,
+        'no-new': 2,
+        'new-cap': 2,
+        'no-eval': 2,
+        'no-invalid-this': 2,
+        radix: [2, 'always'],
+        'no-use-before-define': [2, 'nofunc'],
+        'callback-return': [2, ['callback', 'cb', 'done']],
+        'comma-dangle': [2, 'never'],
+        'comma-style': [2, 'last'],
+        'no-regex-spaces': 2,
+        'no-empty': 2,
+        'no-duplicate-case': 2,
+        'no-empty-character-class': 2,
+        'no-redeclare': [2, {
+            builtinGlobals: true
+        }],
+        'block-scoped-var': 2,
+        'no-sequences': 2,
+        'no-throw-literal': 2,
+        'no-useless-concat': 2,
+        'no-void': 2,
+        yoda: 2,
+        'no-bitwise': 2,
+        'no-lonely-if': 2,
+        'no-mixed-spaces-and-tabs': 2,
+        'no-console': 0
+    },
+    env: {
+        es6: false,
+        node: true
+    },
+    extends: 'eslint:recommended',
+    fix: true,
+    globals: {
+        Promise: false
+    }
+};
diff --git a/node_modules/nodemailer-smtp-transport/.npmignore b/node_modules/nodemailer-smtp-transport/.npmignore
new file mode 100644
index 0000000..b8af069
--- /dev/null
+++ b/node_modules/nodemailer-smtp-transport/.npmignore
@@ -0,0 +1,2 @@
+.travis.yml
+test
\ No newline at end of file
diff --git a/node_modules/nodemailer-smtp-transport/Gruntfile.js b/node_modules/nodemailer-smtp-transport/Gruntfile.js
new file mode 100644
index 0000000..7b80831
--- /dev/null
+++ b/node_modules/nodemailer-smtp-transport/Gruntfile.js
@@ -0,0 +1,27 @@
+'use strict';
+
+module.exports = function (grunt) {
+
+    // Project configuration.
+    grunt.initConfig({
+        eslint: {
+            all: ['lib/*.js', 'test/*.js']
+        },
+
+        mochaTest: {
+            all: {
+                options: {
+                    reporter: 'spec'
+                },
+                src: ['test/*-test.js']
+            }
+        }
+    });
+
+    // Load the plugin(s)
+    grunt.loadNpmTasks('grunt-eslint');
+    grunt.loadNpmTasks('grunt-mocha-test');
+
+    // Tasks
+    grunt.registerTask('default', ['eslint', 'mochaTest']);
+};
diff --git a/node_modules/nodemailer-smtp-transport/LICENSE b/node_modules/nodemailer-smtp-transport/LICENSE
new file mode 100644
index 0000000..02fccdb
--- /dev/null
+++ b/node_modules/nodemailer-smtp-transport/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2014-2016 Andris Reinman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/node_modules/nodemailer-smtp-transport/README.md b/node_modules/nodemailer-smtp-transport/README.md
new file mode 100644
index 0000000..5901456
--- /dev/null
+++ b/node_modules/nodemailer-smtp-transport/README.md
@@ -0,0 +1,7 @@
+# nodemailer-smtp-transport
+
+![Nodemailer](https://raw.githubusercontent.com/nodemailer/nodemailer/master/assets/nm_logo_200x136.png)
+
+SMTP module with for Nodemailer.
+
+See [Nodemailer homepage](https://nodemailer.com/smtp/) for documentation and terms of using SMTP.
diff --git a/node_modules/nodemailer-smtp-transport/lib/smtp-transport.js b/node_modules/nodemailer-smtp-transport/lib/smtp-transport.js
new file mode 100644
index 0000000..5948e44
--- /dev/null
+++ b/node_modules/nodemailer-smtp-transport/lib/smtp-transport.js
@@ -0,0 +1,281 @@
+'use strict';
+
+var SMTPConnection = require('smtp-connection');
+var packageData = require('../package.json');
+var wellknown = require('nodemailer-wellknown');
+var shared = require('nodemailer-shared');
+
+var EventEmitter = require('events').EventEmitter;
+var util = require('util');
+
+// expose to the world
+module.exports = function (options) {
+    return new SMTPTransport(options);
+};
+
+/**
+ * Creates a SMTP transport object for Nodemailer
+ *
+ * @constructor
+ * @param {Object} options Connection options
+ */
+function SMTPTransport(options) {
+    EventEmitter.call(this);
+
+    options = options || {};
+    if (typeof options === 'string') {
+        options = {
+            url: options
+        };
+    }
+
+    var urlData;
+    var service = options.service;
+
+    if (typeof options.getSocket === 'function') {
+        this.getSocket = options.getSocket;
+    }
+
+    if (options.url) {
+        urlData = shared.parseConnectionUrl(options.url);
+        service = service || urlData.service;
+    }
+
+    this.options = assign(
+        false, // create new object
+        options, // regular options
+        urlData, // url options
+        service && wellknown(service) // wellknown options
+    );
+
+    this.logger = shared.getLogger(this.options);
+
+    // temporary object
+    var connection = new SMTPConnection(this.options);
+
+    this.name = 'SMTP';
+    this.version = packageData.version + '[client:' + connection.version + ']';
+}
+util.inherits(SMTPTransport, EventEmitter);
+
+/**
+ * Placeholder function for creating proxy sockets. This method immediatelly returns
+ * without a socket
+ *
+ * @param {Object} options Connection options
+ * @param {Function} callback Callback function to run with the socket keys
+ */
+SMTPTransport.prototype.getSocket = function (options, callback) {
+    // return immediatelly
+    return callback(null, false);
+};
+
+/**
+ * Sends an e-mail using the selected settings
+ *
+ * @param {Object} mail Mail object
+ * @param {Function} callback Callback function
+ */
+SMTPTransport.prototype.send = function (mail, callback) {
+
+    this.getSocket(this.options, function (err, socketOptions) {
+        if (err) {
+            return callback(err);
+        }
+
+        var options = this.options;
+        if (socketOptions && socketOptions.connection) {
+            this.logger.info('Using proxied socket from %s:%s to %s:%s', socketOptions.connection.remoteAddress, socketOptions.connection.remotePort, options.host || '', options.port || '');
+            // only copy options if we need to modify it
+            options = assign(false, options);
+            Object.keys(socketOptions).forEach(function (key) {
+                options[key] = socketOptions[key];
+            });
+        }
+
+        var connection = new SMTPConnection(options);
+        var returned = false;
+
+        connection.once('error', function (err) {
+            if (returned) {
+                return;
+            }
+            returned = true;
+            connection.close();
+            return callback(err);
+        });
+
+        connection.once('end', function () {
+            if (returned) {
+                return;
+            }
+            returned = true;
+            return callback(new Error('Connection closed'));
+        });
+
+        var sendMessage = function () {
+            var envelope = mail.message.getEnvelope();
+            var messageId = (mail.message.getHeader('message-id') || '').replace(/[<>\s]/g, '');
+            var recipients = [].concat(envelope.to || []);
+            if (recipients.length > 3) {
+                recipients.push('...and ' + recipients.splice(2).length + ' more');
+            }
+
+            this.logger.info('Sending message <%s> to <%s>', messageId, recipients.join(', '));
+
+            connection.send(envelope, mail.message.createReadStream(), function (err, info) {
+                if (returned) {
+                    return;
+                }
+                returned = true;
+
+                connection.close();
+                if (err) {
+                    return callback(err);
+                }
+                info.envelope = {
+                    from: envelope.from,
+                    to: envelope.to
+                };
+                info.messageId = messageId;
+                return callback(null, info);
+            });
+        }.bind(this);
+
+        connection.connect(function () {
+            if (returned) {
+                return;
+            }
+
+            if (this.options.auth) {
+                connection.login(this.options.auth, function (err) {
+                    if (returned) {
+                        return;
+                    }
+
+                    if (err) {
+                        returned = true;
+                        connection.close();
+                        return callback(err);
+                    }
+
+                    sendMessage();
+                });
+            } else {
+                sendMessage();
+            }
+        }.bind(this));
+    }.bind(this));
+};
+
+/**
+ * Verifies SMTP configuration
+ *
+ * @param {Function} callback Callback function
+ */
+SMTPTransport.prototype.verify = function (callback) {
+    var promise;
+
+    if (!callback && typeof Promise === 'function') {
+        promise = new Promise(function (resolve, reject) {
+            callback = shared.callbackPromise(resolve, reject);
+        });
+    }
+
+    this.getSocket(this.options, function (err, socketOptions) {
+        if (err) {
+            return callback(err);
+        }
+
+        var options = this.options;
+        if (socketOptions && socketOptions.connection) {
+            this.logger.info('Using proxied socket from %s:%s', socketOptions.connection.remoteAddress, socketOptions.connection.remotePort);
+            options = assign(false, options);
+            Object.keys(socketOptions).forEach(function (key) {
+                options[key] = socketOptions[key];
+            });
+        }
+
+        var connection = new SMTPConnection(options);
+        var returned = false;
+
+        connection.once('error', function (err) {
+            if (returned) {
+                return;
+            }
+            returned = true;
+            connection.close();
+            return callback(err);
+        });
+
+        connection.once('end', function () {
+            if (returned) {
+                return;
+            }
+            returned = true;
+            return callback(new Error('Connection closed'));
+        });
+
+        var finalize = function () {
+            if (returned) {
+                return;
+            }
+            returned = true;
+            connection.quit();
+            return callback(null, true);
+        };
+
+        connection.connect(function () {
+            if (returned) {
+                return;
+            }
+
+            if (this.options.auth) {
+                connection.login(this.options.auth, function (err) {
+                    if (returned) {
+                        return;
+                    }
+
+                    if (err) {
+                        returned = true;
+                        connection.close();
+                        return callback(err);
+                    }
+
+                    finalize();
+                });
+            } else {
+                finalize();
+            }
+        }.bind(this));
+    }.bind(this));
+
+    return promise;
+};
+
+/**
+ * Copies properties from source objects to target objects
+ */
+function assign( /* target, ... sources */ ) {
+    var args = Array.prototype.slice.call(arguments);
+    var target = args.shift() || {};
+
+    args.forEach(function (source) {
+        Object.keys(source || {}).forEach(function (key) {
+            if (['tls', 'auth'].indexOf(key) >= 0 && source[key] && typeof source[key] === 'object') {
+                // tls and auth are special keys that need to be enumerated separately
+                // other objects are passed as is
+                if (!target[key]) {
+                    // esnure that target has this key
+                    target[key] = {};
+                }
+                Object.keys(source[key]).forEach(function (subKey) {
+                    target[key][subKey] = source[key][subKey];
+                });
+            } else {
+                target[key] = source[key];
+            }
+        });
+    });
+    return target;
+}
diff --git a/node_modules/nodemailer-smtp-transport/package.json b/node_modules/nodemailer-smtp-transport/package.json
new file mode 100644
index 0000000..605412c
--- /dev/null
+++ b/node_modules/nodemailer-smtp-transport/package.json
@@ -0,0 +1,101 @@
+{
+  "_args": [
+    [
+      {
+        "raw": "nodemailer-smtp-transport",
+        "scope": null,
+        "escapedName": "nodemailer-smtp-transport",
+        "name": "nodemailer-smtp-transport",
+        "rawSpec": "",
+        "spec": "latest",
+        "type": "tag"
+      },
+      "/Users/fzy/project/koa2_Sequelize_project"
+    ]
+  ],
+  "_from": "nodemailer-smtp-transport@latest",
+  "_id": "nodemailer-smtp-transport@2.7.4",
+  "_inCache": true,
+  "_location": "/nodemailer-smtp-transport",
+  "_nodeVersion": "6.10.0",
+  "_npmOperationalInternal": {
+    "host": "packages-18-east.internal.npmjs.com",
+    "tmp": "tmp/nodemailer-smtp-transport-2.7.4.tgz_1491561468821_0.48228672542609274"
+  },
+  "_npmUser": {
+    "name": "andris",
+    "email": "andris@kreata.ee"
+  },
+  "_npmVersion": "3.10.10",
+  "_phantomChildren": {},
+  "_requested": {
+    "raw": "nodemailer-smtp-transport",
+    "scope": null,
+    "escapedName": "nodemailer-smtp-transport",
+    "name": "nodemailer-smtp-transport",
+    "rawSpec": "",
+    "spec": "latest",
+    "type": "tag"
+  },
+  "_requiredBy": [
+    "#USER",
+    "/"
+  ],
+  "_resolved": "https://registry.npmjs.org/nodemailer-smtp-transport/-/nodemailer-smtp-transport-2.7.4.tgz",
+  "_shasum": "0d89af019a144a480fd8ecc99997d9f838f13685",
+  "_shrinkwrap": null,
+  "_spec": "nodemailer-smtp-transport",
+  "_where": "/Users/fzy/project/koa2_Sequelize_project",
+  "author": {
+    "name": "Andris Reinman"
+  },
+  "bugs": {
+    "url": "https://github.com/andris9/nodemailer-smtp-transport/issues"
+  },
+  "dependencies": {
+    "nodemailer-shared": "1.1.0",
+    "nodemailer-wellknown": "0.1.10",
+    "smtp-connection": "2.12.0"
+  },
+  "description": "SMTP transport for Nodemailer",
+  "devDependencies": {
+    "chai": "^3.5.0",
+    "grunt": "^1.0.1",
+    "grunt-cli": "^1.2.0",
+    "grunt-eslint": "^19.0.0",
+    "grunt-mocha-test": "^0.12.7",
+    "mocha": "^3.0.2",
+    "smtp-server": "^1.14.2"
+  },
+  "directories": {},
+  "dist": {
+    "shasum": "0d89af019a144a480fd8ecc99997d9f838f13685",
+    "tarball": "https://registry.npmjs.org/nodemailer-smtp-transport/-/nodemailer-smtp-transport-2.7.4.tgz"
+  },
+  "gitHead": "d854aa0e4e7b22cd921ae3e4e04ab7b6e02761e6",
+  "homepage": "http://github.com/andris9/nodemailer-smtp-transport",
+  "keywords": [
+    "SMTP",
+    "Nodemailer"
+  ],
+  "license": "MIT",
+  "main": "lib/smtp-transport.js",
+  "maintainers": [
+    {
+      "name": "andris",
+      "email": "andris@node.ee"
+    }
+  ],
+  "name": "nodemailer-smtp-transport",
+  "optionalDependencies": {},
+  "readme": "# nodemailer-smtp-transport\n\n![Nodemailer](https://raw.githubusercontent.com/nodemailer/nodemailer/master/assets/nm_logo_200x136.png)\n\nSMTP module with for Nodemailer.\n\nSee [Nodemailer homepage](https://nodemailer.com/smtp/) for documentation and terms of using SMTP.\n",
+  "readmeFilename": "README.md",
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/andris9/nodemailer-smtp-transport.git"
+  },
+  "scripts": {
+    "test": "grunt mochaTest"
+  },
+  "version": "2.7.4"
+}
diff --git a/node_modules/nodemailer-wellknown/.npmignore b/node_modules/nodemailer-wellknown/.npmignore
new file mode 100644
index 0000000..28f1ba7
--- /dev/null
+++ b/node_modules/nodemailer-wellknown/.npmignore
@@ -0,0 +1,2 @@
+node_modules
+.DS_Store
\ No newline at end of file
diff --git a/node_modules/nodemailer-wellknown/.travis.yml b/node_modules/nodemailer-wellknown/.travis.yml
new file mode 100644
index 0000000..96f3320
--- /dev/null
+++ b/node_modules/nodemailer-wellknown/.travis.yml
@@ -0,0 +1,17 @@
+language: node_js
+sudo: false
+node_js:
+  - "0.10"
+  - 0.12
+  - iojs
+  - 4
+  - 5
+notifications:
+  email:
+    - andris@kreata.ee
+  webhooks:
+    urls:
+      - https://webhooks.gitter.im/e/0ed18fd9b3e529b3c2cc
+    on_success: change  # options: [always|never|change] default: always
+    on_failure: always  # options: [always|never|change] default: always
+    on_start: false     # default: false
diff --git a/node_modules/nodemailer-wellknown/LICENSE b/node_modules/nodemailer-wellknown/LICENSE
new file mode 100644
index 0000000..02fccdb
--- /dev/null
+++ b/node_modules/nodemailer-wellknown/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2014-2016 Andris Reinman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/node_modules/nodemailer-wellknown/README.md b/node_modules/nodemailer-wellknown/README.md
new file mode 100644
index 0000000..e4a23fc
--- /dev/null
+++ b/node_modules/nodemailer-wellknown/README.md
@@ -0,0 +1,80 @@
+# Nodemailer Well-Known Services
+
+Returns SMTP configuration for well-known services
+
+## Usage
+
+Install with npm
+
+    npm install nodemailer-wellknown
+
+Require in your script
+
+```javascript
+var wellknown = require('nodemailer-wellknown');
+```
+
+Resolve SMTP settings
+
+```javascript
+var config = wellknown('Gmail');
+// { host: 'smtp.gmail.com',
+//   port: 465,
+//   secure: true }
+```
+
+## Supported services
+
+Service names are case insensitive
+
+  * **'1und1'**
+  * **'AOL'**
+  * **'DebugMail.io'**
+  * **'DynectEmail'**
+  * **'FastMail'**
+  * **'GandiMail'**
+  * **'Gmail'**
+  * **'Godaddy'**
+  * **'GodaddyAsia'**
+  * **'GodaddyEurope'**
+  * **'hot.ee'**
+  * **'Hotmail'**
+  * **'iCloud'**
+  * **'mail.ee'**
+  * **'Mail.ru'**
+  * **'Mailgun'**
+  * **'Mailjet'**
+  * **'Mandrill'**
+  * **'Naver'**
+  * **'OpenMailBox'**
+  * **'Postmark'**
+  * **'QQ'**
+  * **'QQex'**
+  * **'SendCloud'**
+  * **'SendGrid'**
+  * **'SES'**
+  * **'SES-US-EAST-1'**
+  * **'SES-US-WEST-2'**
+  * **'SES-EU-WEST-1'**
+  * **'Sparkpost'**
+  * **'Yahoo'**
+  * **'Yandex'**
+  * **'Zoho'**
+
+### Example usage with Nodemailer
+
+> **NB!** This repo might be updated more often than Nodemailer itself, so in case
+> a wellknown host is not working, check that you have the latest version of
+> nodemailer-wellknown installed in your node_modules. Otherwise the data you try
+> to use might be still missing.
+
+```javascript
+var transporter = nodemailer.createTransport({
+     service: 'postmark' // <- resolved as 'Postmark' from the wellknown info
+     auth: {...}
+});
+```
+
+## License
+
+**MIT**
diff --git a/node_modules/nodemailer-wellknown/index.js b/node_modules/nodemailer-wellknown/index.js
new file mode 100644
index 0000000..0add9e4
--- /dev/null
+++ b/node_modules/nodemailer-wellknown/index.js
@@ -0,0 +1,47 @@
+'use strict';
+
+var services = require('./services.json');
+var normalized = {};
+
+Object.keys(services).forEach(function(key) {
+    var service = services[key];
+
+    normalized[normalizeKey(key)] = normalizeService(service);
+
+    [].concat(service.aliases || []).forEach(function(alias) {
+        normalized[normalizeKey(alias)] = normalizeService(service);
+    });
+
+    [].concat(service.domains || []).forEach(function(domain) {
+        normalized[normalizeKey(domain)] = normalizeService(service);
+    });
+});
+
+function normalizeKey(key) {
+    return key.replace(/[^a-zA-Z0-9.\-]/g, '').toLowerCase();
+}
+
+function normalizeService(service) {
+    var filter = ['domains', 'aliases'];
+    var response = {};
+
+    Object.keys(service).forEach(function(key) {
+        if (filter.indexOf(key) < 0) {
+            response[key] = service[key];
+        }
+    });
+
+    return response;
+}
+
+/**
+ * Resolves SMTP config for given key. Key can be a name (like 'Gmail'), alias (like 'Google Mail') or
+ * an email address (like 'test@googlemail.com').
+ *
+ * @param {String} key [description]
+ * @returns {Object} SMTP config or false if not found
+ */
+module.exports = function(key) {
+    key = normalizeKey(key.split('@').pop());
+    return normalized[key] || false;
+};
\ No newline at end of file
diff --git a/node_modules/nodemailer-wellknown/package.json b/node_modules/nodemailer-wellknown/package.json
new file mode 100644
index 0000000..81e6e81
--- /dev/null
+++ b/node_modules/nodemailer-wellknown/package.json
@@ -0,0 +1,90 @@
+{
+  "_args": [
+    [
+      {
+        "raw": "nodemailer-wellknown@0.1.10",
+        "scope": null,
+        "escapedName": "nodemailer-wellknown",
+        "name": "nodemailer-wellknown",
+        "rawSpec": "0.1.10",
+        "spec": "0.1.10",
+        "type": "version"
+      },
+      "/Users/fzy/project/koa2_Sequelize_project/node_modules/nodemailer-smtp-transport"
+    ]
+  ],
+  "_from": "nodemailer-wellknown@0.1.10",
+  "_id": "nodemailer-wellknown@0.1.10",
+  "_inCache": true,
+  "_location": "/nodemailer-wellknown",
+  "_nodeVersion": "6.0.0",
+  "_npmOperationalInternal": {
+    "host": "packages-16-east.internal.npmjs.com",
+    "tmp": "tmp/nodemailer-wellknown-0.1.10.tgz_1463170013658_0.2676137052476406"
+  },
+  "_npmUser": {
+    "name": "andris",
+    "email": "andris@kreata.ee"
+  },
+  "_npmVersion": "3.8.6",
+  "_phantomChildren": {},
+  "_requested": {
+    "raw": "nodemailer-wellknown@0.1.10",
+    "scope": null,
+    "escapedName": "nodemailer-wellknown",
+    "name": "nodemailer-wellknown",
+    "rawSpec": "0.1.10",
+    "spec": "0.1.10",
+    "type": "version"
+  },
+  "_requiredBy": [
+    "/nodemailer-smtp-transport"
+  ],
+  "_resolved": "https://registry.npmjs.org/nodemailer-wellknown/-/nodemailer-wellknown-0.1.10.tgz",
+  "_shasum": "586db8101db30cb4438eb546737a41aad0cf13d5",
+  "_shrinkwrap": null,
+  "_spec": "nodemailer-wellknown@0.1.10",
+  "_where": "/Users/fzy/project/koa2_Sequelize_project/node_modules/nodemailer-smtp-transport",
+  "author": {
+    "name": "Andris Reinman"
+  },
+  "bugs": {
+    "url": "https://github.com/andris9/nodemailer-wellknown/issues"
+  },
+  "dependencies": {},
+  "description": "Well known SMTP services",
+  "devDependencies": {
+    "nodeunit": "^0.9.1"
+  },
+  "directories": {},
+  "dist": {
+    "shasum": "586db8101db30cb4438eb546737a41aad0cf13d5",
+    "tarball": "https://registry.npmjs.org/nodemailer-wellknown/-/nodemailer-wellknown-0.1.10.tgz"
+  },
+  "gitHead": "c9d7b500726f98cab239233490aefc62163a216f",
+  "homepage": "https://github.com/andris9/nodemailer-wellknown",
+  "keywords": [
+    "SMTP",
+    "Nodemailer"
+  ],
+  "license": "MIT",
+  "main": "index.js",
+  "maintainers": [
+    {
+      "name": "andris",
+      "email": "andris@node.ee"
+    }
+  ],
+  "name": "nodemailer-wellknown",
+  "optionalDependencies": {},
+  "readme": "# Nodemailer Well-Known Services\n\nReturns SMTP configuration for well-known services\n\n## Usage\n\nInstall with npm\n\n    npm install nodemailer-wellknown\n\nRequire in your script\n\n```javascript\nvar wellknown = require('nodemailer-wellknown');\n```\n\nResolve SMTP settings\n\n```javascript\nvar config = wellknown('Gmail');\n// { host: 'smtp.gmail.com',\n//   port: 465,\n//   secure: true }\n```\n\n## Supported services\n\nService names are case insensitive\n\n  * **'1und1'**\n  * **'AOL'**\n  * **'DebugMail.io'**\n  * **'DynectEmail'**\n  * **'FastMail'**\n  * **'GandiMail'**\n  * **'Gmail'**\n  * **'Godaddy'**\n  * **'GodaddyAsia'**\n  * **'GodaddyEurope'**\n  * **'hot.ee'**\n  * **'Hotmail'**\n  * **'iCloud'**\n  * **'mail.ee'**\n  * **'Mail.ru'**\n  * **'Mailgun'**\n  * **'Mailjet'**\n  * **'Mandrill'**\n  * **'Naver'**\n  * **'OpenMailBox'**\n  * **'Postmark'**\n  * **'QQ'**\n  * **'QQex'**\n  * **'SendCloud'**\n  * **'SendGrid'**\n  * **'SES'**\n  * **'SES-US-EAST-1'**\n  * **'SES-US-WEST-2'**\n  * **'SES-EU-WEST-1'**\n  * **'Sparkpost'**\n  * **'Yahoo'**\n  * **'Yandex'**\n  * **'Zoho'**\n\n### Example usage with Nodemailer\n\n> **NB!** This repo might be updated more often than Nodemailer itself, so in case\n> a wellknown host is not working, check that you have the latest version of\n> nodemailer-wellknown installed in your node_modules. Otherwise the data you try\n> to use might be still missing.\n\n```javascript\nvar transporter = nodemailer.createTransport({\n     service: 'postmark' // <- resolved as 'Postmark' from the wellknown info\n     auth: {...}\n});\n```\n\n## License\n\n**MIT**\n",
+  "readmeFilename": "README.md",
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/andris9/nodemailer-wellknown.git"
+  },
+  "scripts": {
+    "test": "nodeunit test.js"
+  },
+  "version": "0.1.10"
+}
diff --git a/node_modules/nodemailer-wellknown/services.json b/node_modules/nodemailer-wellknown/services.json
new file mode 100644
index 0000000..6f74484
--- /dev/null
+++ b/node_modules/nodemailer-wellknown/services.json
@@ -0,0 +1,255 @@
+{
+    "1und1": {
+        "host": "smtp.1und1.de",
+        "port": 465,
+        "secure": true,
+        "authMethod": "LOGIN"
+    },
+
+    "AOL": {
+        "domains": [
+            "aol.com"
+        ],
+        "host": "smtp.aol.com",
+        "port": 587
+    },
+
+    "DebugMail": {
+        "host": "debugmail.io",
+        "port": 25
+    },
+
+    "DynectEmail": {
+        "aliases": ["Dynect"],
+        "host": "smtp.dynect.net",
+        "port": 25
+    },
+
+    "FastMail": {
+        "domains": [
+            "fastmail.fm"
+        ],
+        "host": "mail.messagingengine.com",
+        "port": 465,
+        "secure": true
+    },
+
+    "GandiMail": {
+        "aliases": [
+            "Gandi",
+            "Gandi Mail"
+        ],
+        "host": "mail.gandi.net",
+        "port": 587
+    },
+
+    "Gmail": {
+        "aliases": [
+            "Google Mail"
+        ],
+        "domains": [
+            "gmail.com",
+            "googlemail.com"
+        ],
+        "host": "smtp.gmail.com",
+        "port": 465,
+        "secure": true
+    },
+
+    "Godaddy": {
+        "host": "smtpout.secureserver.net",
+        "port": 25
+    },
+
+    "GodaddyAsia": {
+        "host": "smtp.asia.secureserver.net",
+        "port": 25
+    },
+
+    "GodaddyEurope": {
+        "host": "smtp.europe.secureserver.net",
+        "port": 25
+    },
+
+    "hot.ee": {
+        "host": "mail.hot.ee"
+    },
+
+    "Hotmail": {
+        "aliases": [
+            "Outlook",
+            "Outlook.com",
+            "Hotmail.com"
+        ],
+        "domains": [
+            "hotmail.com",
+            "outlook.com"
+        ],
+        "host": "smtp.live.com",
+        "port": 587,
+        "tls": {
+            "ciphers": "SSLv3"
+        }
+    },
+
+    "iCloud": {
+        "aliases": ["Me", "Mac"],
+        "domains": [
+            "me.com",
+            "mac.com"
+        ],
+        "host": "smtp.mail.me.com",
+        "port": 587
+    },
+
+    "mail.ee": {
+        "host": "smtp.mail.ee"
+    },
+
+    "Mail.ru": {
+        "host": "smtp.mail.ru",
+        "port": 465,
+        "secure": true
+    },
+
+    "Maildev": {
+        "port": 1025,
+        "ignoreTLS": true
+    },
+
+    "Mailgun": {
+        "host": "smtp.mailgun.org",
+        "port": 587
+    },
+
+    "Mailjet": {
+        "host": "in.mailjet.com",
+        "port": 587
+    },
+
+    "Mandrill": {
+        "host": "smtp.mandrillapp.com",
+        "port": 587
+    },
+
+    "Naver": {
+        "host": "smtp.naver.com",
+        "port": 587
+    },
+
+    "OpenMailBox": {
+        "aliases": [
+            "OMB",
+            "openmailbox.org"
+        ],
+        "host": "smtp.openmailbox.org",
+        "port": 465,
+        "secure": true
+    },
+
+    "Postmark": {
+        "aliases": ["PostmarkApp"],
+        "host": "smtp.postmarkapp.com",
+        "port": 2525
+    },
+
+    "QQ": {
+        "domains": [
+            "qq.com"
+        ],
+        "host": "smtp.qq.com",
+        "port": 465,
+        "secure": true
+    },
+
+    "QQex": {
+        "aliases": ["QQ Enterprise"],
+        "domains": [
+            "exmail.qq.com"
+        ],
+        "host": "smtp.exmail.qq.com",
+        "port": 465,
+        "secure": true
+    },
+
+    "SendCloud": {
+        "host": "smtpcloud.sohu.com",
+        "port": 25
+    },
+
+    "SendGrid": {
+        "host": "smtp.sendgrid.net",
+        "port": 587
+    },
+
+    "SES": {
+        "host": "email-smtp.us-east-1.amazonaws.com",
+        "port": 465,
+        "secure": true
+    },
+    "SES-US-EAST-1": {
+        "host": "email-smtp.us-east-1.amazonaws.com",
+        "port": 465,
+        "secure": true
+    },
+    "SES-US-WEST-2": {
+        "host": "email-smtp.us-west-2.amazonaws.com",
+        "port": 465,
+        "secure": true
+    },
+    "SES-EU-WEST-1": {
+        "host": "email-smtp.eu-west-1.amazonaws.com",
+        "port": 465,
+        "secure": true
+    },
+    
+
+    "Sparkpost": {
+        "aliases": [
+            "SparkPost",
+            "SparkPost Mail"
+        ],
+        "domains": [
+            "sparkpost.com"
+        ],
+        "host": "smtp.sparkpostmail.com",
+        "port": 587,
+        "secure": false
+    },
+
+    "Yahoo": {
+        "domains": [
+            "yahoo.com"
+        ],
+        "host": "smtp.mail.yahoo.com",
+        "port": 465,
+        "secure": true
+    },
+
+    "Yandex": {
+        "domains": [
+            "yandex.ru"
+        ],
+        "host": "smtp.yandex.ru",
+        "port": 465,
+        "secure": true
+    },
+
+    "Zoho": {
+        "host": "smtp.zoho.com",
+        "port": 465,
+        "secure": true,
+        "authMethod": "LOGIN"
+    },
+    "126": {
+        "host": "smtp.126.com",
+        "port": 465,
+        "secure": true
+    },
+    "163": {
+        "host": "smtp.163.com",
+        "port": 465,
+        "secure": true
+    }
+
+}
diff --git a/node_modules/nodemailer-wellknown/test.js b/node_modules/nodemailer-wellknown/test.js
new file mode 100644
index 0000000..1edf265
--- /dev/null
+++ b/node_modules/nodemailer-wellknown/test.js
@@ -0,0 +1,23 @@
+'use strict';
+
+var wellknown = require('./index');
+
+module.exports['Find by key'] = function(test) {
+    test.ok(wellknown('Gmail'));
+    test.done();
+};
+
+module.exports['Find by alias'] = function(test) {
+    test.ok(wellknown('Google Mail'));
+    test.done();
+};
+
+module.exports['Find by domain'] = function(test) {
+    test.ok(wellknown('GoogleMail.com'));
+    test.done();
+};
+
+module.exports['No match'] = function(test) {
+    test.ok(!wellknown('zzzzzz'));
+    test.done();
+};
diff --git a/node_modules/nodemailer/.npmignore b/node_modules/nodemailer/.npmignore
new file mode 100644
index 0000000..d683f0c
--- /dev/null
+++ b/node_modules/nodemailer/.npmignore
@@ -0,0 +1,9 @@
+assets
+test
+examples
+.eslintrc
+.gitignore
+.travis.yml
+Gruntfile.js
+ISSUE_TEMPLATE.md
+CONTRIBUTING.md
diff --git a/node_modules/nodemailer/CHANGELOG.md b/node_modules/nodemailer/CHANGELOG.md
new file mode 100644
index 0000000..720af24
--- /dev/null
+++ b/node_modules/nodemailer/CHANGELOG.md
@@ -0,0 +1,437 @@
+# CHANGELOG
+
+## v4.0.1 2017-04-13
+
+- Fixed issue with LMTP and STARTTLS
+
+## v4.0.0 2017-04-06
+
+- License changed from EUPLv1.1 to MIT
+
+## v3.1.8 2017-03-21
+
+- Fixed invalid List-* header generation
+
+## v3.1.7 2017-03-14
+
+- Emit an error if STARTTLS ends with connection being closed
+
+## v3.1.6 2017-03-14
+
+- Expose last server response for smtpConnection
+
+## v3.1.5 2017-03-08
+
+- Fixed SES transport, added missing `response` value
+
+## v3.1.4 2017-02-26
+
+- Fixed DKIM calculation for empty body
+- Ensure linebreak after message content. This fixes DKIM signatures for non-multipart messages where input did not end with a newline
+
+## v3.1.3 2017-02-17
+
+- Fixed missing `transport.verify()` methods for SES transport
+
+## v3.1.2 2017-02-17
+
+- Added missing error handlers for Sendmail, SES and Stream transports. If a messages contained an invalid URL as attachment then these transports threw an uncatched error
+
+## v3.1.1 2017-02-13
+
+- Fixed missing `transport.on('idle')` and `transport.isIdle()` methods for SES transports
+
+## v3.1.0 2017-02-13
+
+- Added built-in transport for AWS SES. [Docs](http://localhost:1313/transports/ses/)
+- Updated stream transport to allow building JSON strings. [Docs](http://localhost:1313/transports/stream/#json-transport)
+- Added new method _mail.resolveAll_ that fetches all attachments and such to be able to more easily build API-based transports
+
+## v3.0.2 2017-02-04
+
+- Fixed a bug with OAuth2 login where error callback was fired twice if getToken was not available.
+
+## v3.0.1 2017-02-03
+
+- Fixed a bug where Nodemailer threw an exception if `disableFileAccess` option was used
+- Added FLOSS [exception declaration](FLOSS_EXCEPTIONS.md)
+
+## v3.0.0 2017-01-31
+
+- Initial version of Nodemailer 3
+
+This update brings a lot of breaking changes:
+
+- License changed from MIT to **EUPL-1.1**. This was possible as the new version of Nodemailer is a major rewrite. The features I don't have ownership for, were removed or reimplemented. If there's still some snippets in the code that have vague ownership then notify <andris@kreata.ee> about the conflicting code and I'll fix it.
+- Requires **Node.js v6+**
+- All **templating is gone**. It was too confusing to use and to be really universal a huge list of different renderers would be required. Nodemailer is about email, not about parsing different template syntaxes
+- **No NTLM authentication**. It was too difficult to re-implement. If you still need it then it would be possible to introduce a pluggable SASL interface where you could load the NTLM module in your own code and pass it to Nodemailer. Currently this is not possible.
+- **OAuth2 authentication** is built in and has a different [configuration](https://nodemailer.com/smtp/oauth2/). You can use both user (3LO) and service (2LO) accounts to generate access tokens from Nodemailer. Additionally there's a new feature to authenticate differently for every message – useful if your application sends on behalf of different users instead of a single sender.
+- **Improved Calendaring**. Provide an ical file to Nodemailer to send out [calendar events](https://nodemailer.com/message/calendar-events/).
+
+And also some non-breaking changes:
+
+- All **dependencies were dropped**. There is exactly 0 dependencies needed to use Nodemailer. This brings the installation time of Nodemailer from NPM down to less than 2 seconds
+- **Delivery status notifications** added to Nodemailer
+- Improved and built-in **DKIM** signing of messages. Previously you needed an external module for this and it did quite a lousy job with larger messages
+- **Stream transport** to return a RFC822 formatted message as a stream. Useful if you want to use Nodemailer as a preprocessor and not for actual delivery.
+- **Sendmail** transport built-in, no need for external transport plugin
+
+See [Nodemailer.com](https://nodemailer.com/) for full documentation
+
+## 2.7.0 2016-12-08
+
+- Bumped mailcomposer that generates encoded-words differently which might break some tests
+
+## 2.6.0 2016-09-05
+
+- Added new options disableFileAccess and disableUrlAccess
+- Fixed envelope handling where cc/bcc fields were ignored in the envelope object
+
+## 2.4.2 2016-05-25
+
+- Removed shrinkwrap file. Seemed to cause more trouble than help
+
+## 2.4.1 2016-05-12
+
+- Fixed outdated shrinkwrap file
+
+## 2.4.0 2016-05-11
+
+- Bumped mailcomposer module to allow using `false` as attachment filename (suppresses filename usage)
+- Added NTLM authentication support
+
+## 2.3.2 2016-04-11
+
+- Bumped smtp transport modules to get newest smtp-connection that fixes SMTPUTF8 support for internationalized email addresses
+
+## 2.3.1 2016-04-08
+
+- Bumped mailcomposer to have better support for message/822 attachments
+
+## 2.3.0 2016-03-03
+
+- Fixed a bug with attachment filename that contains mixed unicode and dashes
+- Added built-in support for proxies by providing a new SMTP option `proxy` that takes a proxy configuration url as its value
+- Added option `transport` to dynamically load transport plugins
+- Do not require globally installed grunt-cli
+
+## 2.2.1 2016-02-20
+
+- Fixed a bug in SMTP requireTLS option that was broken
+
+## 2.2.0 2016-02-18
+
+- Removed the need to use `clone` dependency
+- Added new method `verify` to check SMTP configuration
+- Direct transport uses STARTTLS by default, fallbacks to plaintext if STARTTLS fails
+- Added new message option `list` for setting List-* headers
+- Add simple proxy support with `getSocket` method
+- Added new message option `textEncoding`. If `textEncoding` is not set then detect best encoding automatically
+- Added new message option `icalEvent` to embed iCalendar events. Example [here](examples/ical-event.js)
+- Added new attachment option `raw` to use prepared MIME contents instead of generating a new one. This might be useful when you want to handcraft some parts of the message yourself, for example if you want to inject a PGP encrypted message as the contents of a MIME node
+- Added new message option `raw` to use an existing MIME message instead of generating a new one
+
+## 2.1.0 2016-02-01
+
+Republishing 2.1.0-rc.1 as stable. To recap, here's the notable changes between v2.0 and v2.1:
+
+- Implemented templating support. You can either use a simple built-in renderer or some external advanced renderer, eg. [node-email-templates](https://github.com/niftylettuce/node-email-templates). Templating [docs](http://nodemailer.com/2-0-0-beta/templating/).
+- Updated smtp-pool to emit 'idle' events in order to handle message queue more effectively
+- Updated custom header handling, works everywhere the same now, no differences between adding custom headers to the message or to an attachment
+
+## 2.1.0-rc.1 2016-01-25
+
+Sneaked in some new features even though it is already rc
+
+- If a SMTP pool is closed while there are still messages in a queue, the message callbacks are invoked with an error
+- In case of SMTP pool the transporter emits 'idle' when there is a free connection slot available
+- Added method `isIdle()` that checks if a pool has still some free connection slots available
+
+## 2.1.0-rc.0 2016-01-20
+
+- Bumped dependency versions
+
+## 2.1.0-beta.3 2016-01-20
+
+- Added support for node-email-templates templating in addition to the built-in renderer
+
+## 2.1.0-beta.2 2016-01-20
+
+- Implemented simple templating feature
+
+## 2.1.0-beta.1 2016-01-20
+
+- Allow using prepared header values that are not folded or encoded by Nodemailer
+
+## 2.1.0-beta.0 2016-01-20
+
+- Use the same header custom structure for message root, attachments and alternatives
+- Ensure that Message-Id exists when accessing message
+- Allow using array values for custom headers (inserts every value in its own row)
+
+## 2.0.0 2016-01-11
+
+- Released rc.2 as stable
+
+## 2.0.0-rc.2 2016-01-04
+
+- Locked dependencies
+
+## 2.0.0-beta.2 2016-01-04
+
+- Updated documentation to reflect changes with SMTP handling
+- Use beta versions for smtp/pool/direct transports
+- Updated logging
+
+## 2.0.0-beta.1 2016-01-03
+
+- Use bunyan compatible logger instead of the emit('log') style
+- Outsourced some reusable methods to nodemailer-shared
+- Support setting direct/smtp/pool with the default configuration
+
+## 2.0.0-beta.0 2015-12-31
+
+- Stream errors are not silently swallowed
+- Do not use format=flowed
+- Use nodemailer-fetch to fetch URL streams
+- jshint replaced by eslint
+
+## v1.11.0 2015-12-28
+
+Allow connection url based SMTP configurations
+
+## v1.10.0 2015-11-13
+
+Added `defaults` argument for `createTransport` to predefine commonn values (eg. `from` address)
+
+## v1.9.0 2015-11-09
+
+Returns a Promise for `sendMail` if callback is not defined
+
+## v1.8.0 2015-10-08
+
+Added priority option (high, normal, low) for setting Importance header
+
+## v1.7.0 2015-10-06
+
+Replaced hyperquest with needle. Fixes issues with compressed data and redirects
+
+## v1.6.0 2015-10-05
+
+Maintenance release. Bumped dependencies to get support for unicode filenames for QQ webmail and to support emoji in filenames
+
+## v1.5.0 2015-09-24
+
+Use mailcomposer instead of built in solution to generate message sources. Bumped libmime gives better quoted-printable handling.
+
+## v1.4.0 2015-06-27
+
+Added new message option `watchHtml` to specify Apple Watch specific HTML part of the message. See [this post](https://litmus.com/blog/how-to-send-hidden-version-email-apple-watch) for details
+
+## v1.3.4 2015-04-25
+
+Maintenance release, bumped buildmail version to get fixed format=flowed handling
+
+## v1.3.3 2015-04-25
+
+Maintenance release, bumped dependencies
+
+## v1.3.2 2015-03-09
+
+Maintenance release, upgraded dependencies. Replaced simplesmtp based tests with smtp-server based ones.
+
+## v1.3.0 2014-09-12
+
+Maintenance release, upgrades buildmail and libmime. Allows using functions as transform plugins and fixes issue with unicode filenames in Gmail.
+
+## v1.2.2 2014-09-05
+
+Proper handling of data uris as attachments. Attachment `path` property can also be defined as a data uri, not just regular url or file path.
+
+## v1.2.1 2014-08-21
+
+Bumped libmime and mailbuild versions to properly handle filenames with spaces (short ascii only filenames with spaces were left unquoted).
+
+## v1.2.0 2014-08-18
+
+Allow using encoded strings as attachments. Added new property `encoding` which defines the encoding used for a `content` string. If encoding is set, the content value is converted to a Buffer value using the defined encoding before usage. Useful for including binary attachemnts in JSON formatted email objects.
+
+## v1.1.2 2014-08-18
+
+Return deprecatin error for v0.x style configuration
+
+## v1.1.1 2014-07-30
+
+Bumped nodemailer-direct-transport dependency. Updated version includes a bugfix for Stream nodes handling. Important only if use direct-transport with Streams (not file paths or urls) as attachment content.
+
+## v1.1.0 2014-07-29
+
+Added new method `resolveContent()` to get the html/text/attachment content as a String or Buffer.
+
+## v1.0.4 2014-07-23
+
+Bugfix release. HTML node was instered twice if the message consisted of a HTML content (but no text content) + at least one attachment with CID + at least one attachment without CID. In this case the HTML node was inserted both to the root level multipart/mixed section and to the multipart/related sub section
+
+## v1.0.3 2014-07-16
+
+Fixed a bug where Nodemailer crashed if the message content type was multipart/related
+
+## v1.0.2 2014-07-16
+
+Upgraded nodemailer-smtp-transport to 0.1.11\. The docs state that for SSL you should use 'secure' option but the underlying smtp-connection module used 'secureConnection' for this purpose. Fixed smpt-connection to match the docs.
+
+## v1.0.1 2014-07-15
+
+Implemented missing #close method that is passed to the underlying transport object. Required by the smtp pool.
+
+## v1.0.0 2014-07-15
+
+Total rewrite. See migration guide here: <http://www.andrisreinman.com/nodemailer-v1-0/#migrationguide>
+
+## v0.7.1 2014-07-09
+
+- Upgraded aws-sdk to 2.0.5
+
+## v0.7.0 2014-06-17
+
+- Bumped version to v0.7.0
+- Fix AWS-SES usage [5b6bc144]
+- Replace current SES with new SES using AWS-SDK (Elanorr) [c79d797a]
+- Updated README.md about Node Email Templates (niftylettuce) [e52bef81]
+
+## v0.6.5 2014-05-15
+
+- Bumped version to v0.6.5
+- Use tildes instead of carets for dependency listing [5296ce41]
+- Allow clients to set a custom identityString (venables) [5373287d]
+- bugfix (adding "-i" to sendmail command line for each new mail) by copying this.args (vrodic) [05a8a9a3]
+- update copyright (gdi2290) [3a6cba3a]
+
+## v0.6.4 2014-05-13
+
+- Bumped version to v0.6.4
+- added npmignore, bumped dependencies [21bddcd9]
+- Add AOL to well-known services (msouce) [da7dd3b7]
+
+## v0.6.3 2014-04-16
+
+- Bumped version to v0.6.3
+- Upgraded simplesmtp dependency [dd367f59]
+
+## v0.6.2 2014-04-09
+
+- Bumped version to v0.6.2
+- Added error option to Stub transport [c423acad]
+- Use SVG npm badge (t3chnoboy) [677117b7]
+- add SendCloud to well known services (haio) [43c358e0]
+- High-res build-passing and NPM module badges (sahat) [9fdc37cd]
+
+## v0.6.1 2014-01-26
+
+- Bumped version to v0.6.1
+- Do not throw on multiple errors from sendmail command [c6e2cd12]
+- Do not require callback for pickup, fixes #238 [93eb3214]
+- Added AWSSecurityToken information to README, fixes #235 [58e921d1]
+- Added Nodemailer logo [06b7d1a8]
+
+## v0.6.0 2013-12-30
+
+- Bumped version to v0.6.0
+- Allow defining custom transport methods [ec5b48ce]
+- Return messageId with responseObject for all built in transport methods [74445cec]
+- Bumped dependency versions for mailcomposer and readable-stream [9a034c34]
+- Changed pickup argument name to 'directory' [01c3ea53]
+- Added support for IIS pickup directory with PICKUP transport (philipproplesch) [36940b59..360a2878]
+- Applied common styles [9e93a409]
+- Updated readme [c78075e7]
+
+## v0.5.15 2013-12-13
+
+- bumped version to v0.5.15
+- Updated README, added global options info for setting uo transports [554bb0e5]
+- Resolve public hostname, if resolveHostname property for a transport object is set to `true` [9023a6e1..4c66b819]
+
+## v0.5.14 2013-12-05
+
+- bumped version to v0.5.14
+- Expose status for direct messages [f0312df6]
+- Allow to skip the X-Mailer header if xMailer value is set to 'false' [f2c20a68]
+
+## v0.5.13 2013-12-03
+
+- bumped version to v0.5.13
+- Use the name property from the transport object to use for the domain part of message-id values (1598eee9)
+
+## v0.5.12 2013-12-02
+
+- bumped version to v0.5.12
+- Expose transport method and transport module version if available [a495106e]
+- Added 'he' module instead of using custom html entity decoding [c197d102]
+- Added xMailer property for transport configuration object to override X-Mailer value [e8733a61]
+- Updated README, added description for 'mail' method [e1f5f3a6]
+
+## v0.5.11 2013-11-28
+
+- bumped version to v0.5.11
+- Updated mailcomposer version. Replaces ent with he [6a45b790e]
+
+## v0.5.10 2013-11-26
+
+- bumped version to v0.5.10
+- added shorthand function mail() for direct transport type [88129bd7]
+- minor tweaks and typo fixes [f797409e..ceac0ca4]
+
+## v0.5.9 2013-11-25
+
+- bumped version to v0.5.9
+- Update for 'direct' handling [77b84e2f]
+- do not require callback to be provided for 'direct' type [ec51c79f]
+
+## v0.5.8 2013-11-22
+
+- bumped version to v0.5.8
+- Added support for 'direct' transport [826f226d..0dbbcbbc]
+
+## v0.5.7 2013-11-18
+
+- bumped version to v0.5.7
+- Replace \r\n by \n in Sendmail transport (rolftimmermans) [fed2089e..616ec90c] A lot of sendmail implementations choke on \r\n newlines and require \n This commit addresses this by transforming all \r\n sequences passed to the sendmail command with \n
+
+## v0.5.6 2013-11-15
+
+- bumped version to v0.5.6
+- Upgraded mailcomposer dependency to 0.2.4 [e5ff9c40]
+- Removed noCR option [e810d1b8]
+- Update wellknown.js, added FastMail (k-j-kleist) [cf930f6d]
+
+## v0.5.5 2013-10-30
+
+- bumped version to v0.5.5
+- Updated mailcomposer dependnecy version to 0.2.3
+- Remove legacy code - node v0.4 is not supported anymore anyway
+- Use hostname (autodetected or from the options.name property) for Message-Id instead of "Nodemailer" (helps a bit when messages are identified as spam)
+- Added maxMessages info to README
+
+## v0.5.4 2013-10-29
+
+- bumped version to v0.5.4
+- added "use strict" statements
+- Added DSN info to README
+- add support for QQ enterprise email (coderhaoxin)
+- Add a Bitdeli Badge to README
+- DSN options Passthrought into simplesmtp. (irvinzz)
+
+## v0.5.3 2013-10-03
+
+- bumped version v0.5.3
+- Using a stub transport to prevent sendmail from being called during a test. (jsdevel)
+- closes #78: sendmail transport does not work correctly on Unix machines. (jsdevel)
+- Updated PaaS Support list to include Modulus. (fiveisprime)
+- Translate self closing break tags to newline (kosmasgiannis)
+- fix typos (aeosynth)
+
+## v0.5.2 2013-07-25
+
+- bumped version v0.5.2
+- Merge pull request #177 from MrSwitch/master Fixing Amazon SES, fatal error caused by bad connection
diff --git a/node_modules/nodemailer/LICENSE b/node_modules/nodemailer/LICENSE
new file mode 100644
index 0000000..26b5254
--- /dev/null
+++ b/node_modules/nodemailer/LICENSE
@@ -0,0 +1,16 @@
+Copyright (c) 2011-2017 Andris Reinman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/node_modules/nodemailer/README.md b/node_modules/nodemailer/README.md
new file mode 100644
index 0000000..0180358
--- /dev/null
+++ b/node_modules/nodemailer/README.md
@@ -0,0 +1,15 @@
+# Nodemailer
+
+![Nodemailer](https://raw.githubusercontent.com/nodemailer/nodemailer/master/assets/nm_logo_200x136.png)
+
+Send e-mails from Node.js – easy as cake! 🍰✉️
+
+<a href="https://gitter.im/nodemailer/nodemailer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"><img src="https://badges.gitter.im/Join Chat.svg" alt="Gitter chat" height="18"></a> <a href="http://travis-ci.org/nodemailer/nodemailer"><img src="https://secure.travis-ci.org/nodemailer/nodemailer.svg" alt="Build Status" height="18"></a> <a href="http://badge.fury.io/js/nodemailer"><img src="https://badge.fury.io/js/nodemailer.svg" alt="NPM version" height="18"></a> <a href="https://www.npmjs.com/package/nodemailer"><img src="https://img.shields.io/npm/dt/nodemailer.svg" alt="NPM downloads" height="18"></a>
+
+[![NPM](https://nodei.co/npm/nodemailer.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/nodemailer/)
+
+See [nodemailer.com](https://nodemailer.com/) for documentation and terms.
+
+-------
+
+Nodemailer v4.0.0 and up is licensed under the [MIT license](./LICENSE)
diff --git a/node_modules/nodemailer/lib/addressparser/index.js b/node_modules/nodemailer/lib/addressparser/index.js
new file mode 100644
index 0000000..6a440b4
--- /dev/null
+++ b/node_modules/nodemailer/lib/addressparser/index.js
@@ -0,0 +1,294 @@
+'use strict';
+
+/**
+ * Converts tokens for a single address into an address object
+ *
+ * @param {Array} tokens Tokens object
+ * @return {Object} Address object
+ */
+function _handleAddress(tokens) {
+    let token;
+    let isGroup = false;
+    let state = 'text';
+    let address;
+    let addresses = [];
+    let data = {
+        address: [],
+        comment: [],
+        group: [],
+        text: []
+    };
+    let i;
+    let len;
+
+    // Filter out <addresses>, (comments) and regular text
+    for (i = 0, len = tokens.length; i < len; i++) {
+        token = tokens[i];
+        if (token.type === 'operator') {
+            switch (token.value) {
+                case '<':
+                    state = 'address';
+                    break;
+                case '(':
+                    state = 'comment';
+                    break;
+                case ':':
+                    state = 'group';
+                    isGroup = true;
+                    break;
+                default:
+                    state = 'text';
+            }
+        } else if (token.value) {
+            if (state === 'address') {
+                // handle use case where unquoted name includes a "<"
+                // Apple Mail truncates everything between an unexpected < and an address
+                // and so will we
+                token.value = token.value.replace(/^[^<]*<\s*/, '');
+            }
+            data[state].push(token.value);
+        }
+    }
+
+    // If there is no text but a comment, replace the two
+    if (!data.text.length && data.comment.length) {
+        data.text = data.comment;
+        data.comment = [];
+    }
+
+    if (isGroup) {
+        // http://tools.ietf.org/html/rfc2822#appendix-A.1.3
+        data.text = data.text.join(' ');
+        addresses.push({
+            name: data.text || (address && address.name),
+            group: data.group.length ? addressparser(data.group.join(',')) : []
+        });
+    } else {
+        // If no address was found, try to detect one from regular text
+        if (!data.address.length && data.text.length) {
+            for (i = data.text.length - 1; i >= 0; i--) {
+                if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
+                    data.address = data.text.splice(i, 1);
+                    break;
+                }
+            }
+
+            let _regexHandler = function (address) {
+                if (!data.address.length) {
+                    data.address = [address.trim()];
+                    return ' ';
+                } else {
+                    return address;
+                }
+            };
+
+            // still no address
+            if (!data.address.length) {
+                for (i = data.text.length - 1; i >= 0; i--) {
+                    // fixed the regex to parse email address correctly when email address has more than one @
+                    data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, _regexHandler).trim();
+                    if (data.address.length) {
+                        break;
+                    }
+                }
+            }
+        }
+
+        // If there's still is no text but a comment exixts, replace the two
+        if (!data.text.length && data.comment.length) {
+            data.text = data.comment;
+            data.comment = [];
+        }
+
+        // Keep only the first address occurence, push others to regular text
+        if (data.address.length > 1) {
+            data.text = data.text.concat(data.address.splice(1));
+        }
+
+        // Join values with spaces
+        data.text = data.text.join(' ');
+        data.address = data.address.join(' ');
+
+        if (!data.address && isGroup) {
+            return [];
+        } else {
+            address = {
+                address: data.address || data.text || '',
+                name: data.text || data.address || ''
+            };
+
+            if (address.address === address.name) {
+                if ((address.address || '').match(/@/)) {
+                    address.name = '';
+                } else {
+                    address.address = '';
+                }
+
+            }
+
+            addresses.push(address);
+        }
+    }
+
+    return addresses;
+}
+
+/**
+ * Creates a Tokenizer object for tokenizing address field strings
+ *
+ * @constructor
+ * @param {String} str Address field string
+ */
+class Tokenizer {
+    constructor(str) {
+        this.str = (str || '').toString();
+        this.operatorCurrent = '';
+        this.operatorExpecting = '';
+        this.node = null;
+        this.escaped = false;
+
+        this.list = [];
+        /**
+         * Operator tokens and which tokens are expected to end the sequence
+         */
+        this.operators = {
+            '"': '"',
+            '(': ')',
+            '<': '>',
+            ',': '',
+            ':': ';',
+            // Semicolons are not a legal delimiter per the RFC2822 grammar other
+            // than for terminating a group, but they are also not valid for any
+            // other use in this context.  Given that some mail clients have
+            // historically allowed the semicolon as a delimiter equivalent to the
+            // comma in their UI, it makes sense to treat them the same as a comma
+            // when used outside of a group.
+            ';': ''
+        };
+    }
+
+
+
+    /**
+     * Tokenizes the original input string
+     *
+     * @return {Array} An array of operator|text tokens
+     */
+    tokenize() {
+        let chr, list = [];
+        for (let i = 0, len = this.str.length; i < len; i++) {
+            chr = this.str.charAt(i);
+            this.checkChar(chr);
+        }
+
+        this.list.forEach(node => {
+            node.value = (node.value || '').toString().trim();
+            if (node.value) {
+                list.push(node);
+            }
+        });
+
+        return list;
+    }
+
+    /**
+     * Checks if a character is an operator or text and acts accordingly
+     *
+     * @param {String} chr Character from the address field
+     */
+    checkChar(chr) {
+        if ((chr in this.operators || chr === '\\') && this.escaped) {
+            this.escaped = false;
+        } else if (this.operatorExpecting && chr === this.operatorExpecting) {
+            this.node = {
+                type: 'operator',
+                value: chr
+            };
+            this.list.push(this.node);
+            this.node = null;
+            this.operatorExpecting = '';
+            this.escaped = false;
+            return;
+        } else if (!this.operatorExpecting && chr in this.operators) {
+            this.node = {
+                type: 'operator',
+                value: chr
+            };
+            this.list.push(this.node);
+            this.node = null;
+            this.operatorExpecting = this.operators[chr];
+            this.escaped = false;
+            return;
+        }
+
+        if (!this.escaped && chr === '\\') {
+            this.escaped = true;
+            return;
+        }
+
+        if (!this.node) {
+            this.node = {
+                type: 'text',
+                value: ''
+            };
+            this.list.push(this.node);
+        }
+
+        if (this.escaped && chr !== '\\') {
+            this.node.value += '\\';
+        }
+
+        this.node.value += chr;
+        this.escaped = false;
+    }
+}
+
+/**
+ * Parses structured e-mail addresses from an address field
+ *
+ * Example:
+ *
+ *    'Name <address@domain>'
+ *
+ * will be converted to
+ *
+ *     [{name: 'Name', address: 'address@domain'}]
+ *
+ * @param {String} str Address field
+ * @return {Array} An array of address objects
+ */
+function addressparser(str) {
+    let tokenizer = new Tokenizer(str);
+    let tokens = tokenizer.tokenize();
+
+    let addresses = [];
+    let address = [];
+    let parsedAddresses = [];
+
+    tokens.forEach(token => {
+        if (token.type === 'operator' && (token.value === ',' || token.value === ';')) {
+            if (address.length) {
+                addresses.push(address);
+            }
+            address = [];
+        } else {
+            address.push(token);
+        }
+    });
+
+    if (address.length) {
+        addresses.push(address);
+    }
+
+    addresses.forEach(address => {
+        address = _handleAddress(address);
+        if (address.length) {
+            parsedAddresses = parsedAddresses.concat(address);
+        }
+    });
+
+    return parsedAddresses;
+}
+
+// expose to the world
+module.exports = addressparser;
diff --git a/node_modules/nodemailer/lib/base64/index.js b/node_modules/nodemailer/lib/base64/index.js
new file mode 100644
index 0000000..e2be542
--- /dev/null
+++ b/node_modules/nodemailer/lib/base64/index.js
@@ -0,0 +1,133 @@
+'use strict';
+
+const Transform = require('stream').Transform;
+
+/**
+ * Encodes a Buffer into a base64 encoded string
+ *
+ * @param {Buffer} buffer Buffer to convert
+ * @returns {String} base64 encoded string
+ */
+function encode(buffer) {
+    if (typeof buffer === 'string') {
+        buffer = new Buffer(buffer, 'utf-8');
+    }
+
+    return buffer.toString('base64');
+}
+
+/**
+ * Adds soft line breaks to a base64 string
+ *
+ * @param {String} str base64 encoded string that might need line wrapping
+ * @param {Number} [lineLength=76] Maximum allowed length for a line
+ * @returns {String} Soft-wrapped base64 encoded string
+ */
+function wrap(str, lineLength) {
+    str = (str || '').toString();
+    lineLength = lineLength || 76;
+
+    if (str.length <= lineLength) {
+        return str;
+    }
+
+    let result = [];
+    let pos = 0;
+    let chunkLength = lineLength * 1024;
+    while (pos < str.length) {
+        let wrappedLines = str.substr(pos, chunkLength).replace(new RegExp('.{' + lineLength + '}', 'g'), '$&\r\n').trim();
+        result.push(wrappedLines);
+        pos += chunkLength;
+    }
+
+    return result.join('\r\n').trim();
+}
+
+/**
+ * Creates a transform stream for encoding data to base64 encoding
+ *
+ * @constructor
+ * @param {Object} options Stream options
+ * @param {Number} [options.lineLength=76] Maximum lenght for lines, set to false to disable wrapping
+ */
+class Encoder extends Transform {
+    constructor(options) {
+        super();
+        // init Transform
+        this.options = options || {};
+
+        if (this.options.lineLength !== false) {
+            this.options.lineLength = this.options.lineLength || 76;
+        }
+
+        this._curLine = '';
+        this._remainingBytes = false;
+
+        this.inputBytes = 0;
+        this.outputBytes = 0;
+    }
+
+    _transform(chunk, encoding, done) {
+        let b64;
+
+        if (encoding !== 'buffer') {
+            chunk = new Buffer(chunk, encoding);
+        }
+
+        if (!chunk || !chunk.length) {
+            return done();
+        }
+
+        this.inputBytes += chunk.length;
+
+        if (this._remainingBytes && this._remainingBytes.length) {
+            chunk = Buffer.concat([this._remainingBytes, chunk]);
+            this._remainingBytes = false;
+        }
+
+        if (chunk.length % 3) {
+            this._remainingBytes = chunk.slice(chunk.length - chunk.length % 3);
+            chunk = chunk.slice(0, chunk.length - chunk.length % 3);
+        } else {
+            this._remainingBytes = false;
+        }
+
+        b64 = this._curLine + encode(chunk);
+
+        if (this.options.lineLength) {
+            b64 = wrap(b64, this.options.lineLength);
+            b64 = b64.replace(/(^|\n)([^\n]*)$/, (match, lineBreak, lastLine) => {
+                this._curLine = lastLine;
+                return lineBreak;
+            });
+        }
+
+        if (b64) {
+            this.outputBytes += b64.length;
+            this.push(b64);
+        }
+
+        done();
+    }
+
+    _flush(done) {
+        if (this._remainingBytes && this._remainingBytes.length) {
+            this._curLine += encode(this._remainingBytes);
+        }
+
+        if (this._curLine) {
+            this._curLine = wrap(this._curLine, this.options.lineLength);
+            this.outputBytes += this._curLine.length;
+            this.push(this._curLine, 'ascii');
+            this._curLine = '';
+        }
+        done();
+    }
+}
+
+// expose to the world
+module.exports = {
+    encode,
+    wrap,
+    Encoder
+};
diff --git a/node_modules/nodemailer/lib/dkim/index.js b/node_modules/nodemailer/lib/dkim/index.js
new file mode 100644
index 0000000..d7d1deb
--- /dev/null
+++ b/node_modules/nodemailer/lib/dkim/index.js
@@ -0,0 +1,248 @@
+'use strict';
+
+// FIXME:
+// replace this Transform mess with a method that pipes input argument to output argument
+
+const MessageParser = require('./message-parser');
+const RelaxedBody = require('./relaxed-body');
+const sign = require('./sign');
+const PassThrough = require('stream').PassThrough;
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+
+const DKIM_ALGO = 'sha256';
+const MAX_MESSAGE_SIZE = 128 * 1024; // buffer messages larger than this to disk
+
+/*
+// Usage:
+
+let dkim = new DKIM({
+    domainName: 'example.com',
+    keySelector: 'key-selector',
+    privateKey,
+    cacheDir: '/tmp'
+});
+dkim.sign(input).pipe(process.stdout);
+
+// Where inputStream is a rfc822 message (either a stream, string or Buffer)
+// and outputStream is a DKIM signed rfc822 message
+*/
+
+class DKIMSigner {
+    constructor(options, keys, input, output) {
+        this.options = options || {};
+        this.keys = keys;
+
+        this.cacheTreshold = Number(this.options.cacheTreshold) || MAX_MESSAGE_SIZE;
+        this.hashAlgo = this.options.hashAlgo || DKIM_ALGO;
+
+        this.cacheDir = this.options.cacheDir || false;
+
+        this.chunks = [];
+        this.chunklen = 0;
+        this.readPos = 0;
+        this.cachePath = this.cacheDir ? path.join(this.cacheDir, 'message.' + Date.now() + '-' + crypto.randomBytes(14).toString('hex')) : false;
+        this.cache = false;
+
+        this.headers = false;
+        this.bodyHash = false;
+        this.parser = false;
+        this.relaxedBody = false;
+
+        this.input = input;
+        this.output = output;
+        this.output.usingCache = false;
+
+        this.errored = false;
+
+        this.input.on('error', err => {
+            this.errored = true;
+            this.cleanup();
+            output.emit('error', err);
+        });
+    }
+
+    cleanup() {
+        if (!this.cache || !this.cachePath) {
+            return;
+        }
+        fs.unlink(this.cachePath, () => false);
+    }
+
+    createReadCache() {
+        // pipe remainings to cache file
+        this.cache = fs.createReadStream(this.cachePath);
+        this.cache.once('error', err => {
+            this.cleanup();
+            this.output.emit('error', err);
+        });
+        this.cache.once('close', () => {
+            this.cleanup();
+        });
+        this.cache.pipe(this.output);
+    }
+
+    sendNextChunk() {
+        if (this.errored) {
+            return;
+        }
+
+        if (this.readPos >= this.chunks.length) {
+            if (!this.cache) {
+                return this.output.end();
+            }
+            return this.createReadCache();
+        }
+        let chunk = this.chunks[this.readPos++];
+        if (this.output.write(chunk) === false) {
+            return this.output.once('drain', () => {
+                this.sendNextChunk();
+            });
+        }
+        setImmediate(() => this.sendNextChunk());
+    }
+
+    sendSignedOutput() {
+        let keyPos = 0;
+        let signNextKey = () => {
+            if (keyPos >= this.keys.length) {
+                this.output.write(this.parser.rawHeaders);
+                return setImmediate(() => this.sendNextChunk());
+            }
+            let key = this.keys[keyPos++];
+            let dkimField = sign(this.headers, this.hashAlgo, this.bodyHash, {
+                domainName: key.domainName,
+                keySelector: key.keySelector,
+                privateKey: key.privateKey,
+                headerFieldNames: this.options.headerFieldNames,
+                skipFields: this.options.skipFields
+            });
+            if (dkimField) {
+                this.output.write(Buffer.from(dkimField + '\r\n'));
+            }
+            return setImmediate(signNextKey);
+        };
+
+        if (this.bodyHash && this.headers) {
+            return signNextKey();
+        }
+
+        this.output.write(this.parser.rawHeaders);
+        this.sendNextChunk();
+    }
+
+    createWriteCache() {
+        this.output.usingCache = true;
+        // pipe remainings to cache file
+        this.cache = fs.createWriteStream(this.cachePath);
+        this.cache.once('error', err => {
+            this.cleanup();
+            // drain input
+            this.relaxedBody.unpipe(this.cache);
+            this.relaxedBody.on('readable', () => {
+                while (this.relaxedBody.read() !== null) {
+                    // do nothing
+                }
+            });
+            this.errored = true;
+            // emit error
+            this.output.emit('error', err);
+        });
+        this.cache.once('close', () => {
+            this.sendSignedOutput();
+        });
+        this.relaxedBody.pipe(this.cache);
+    }
+
+    signStream() {
+        this.parser = new MessageParser();
+        this.relaxedBody = new RelaxedBody({
+            hashAlgo: this.hashAlgo
+        });
+
+        this.parser.on('headers', value => {
+            this.headers = value;
+        });
+
+        this.relaxedBody.on('hash', value => {
+            this.bodyHash = value;
+        });
+
+        this.relaxedBody.on('readable', () => {
+            let chunk;
+            if (this.cache) {
+                return;
+            }
+            while ((chunk = this.relaxedBody.read()) !== null) {
+                this.chunks.push(chunk);
+                this.chunklen += chunk.length;
+                if (this.chunklen >= this.cacheTreshold && this.cachePath) {
+                    return this.createWriteCache();
+                }
+            }
+        });
+
+        this.relaxedBody.on('end', () => {
+            if (this.cache) {
+                return;
+            }
+            this.sendSignedOutput();
+        });
+
+        this.parser.pipe(this.relaxedBody);
+        setImmediate(() => this.input.pipe(this.parser));
+    }
+}
+
+class DKIM {
+    constructor(options) {
+        this.options = options || {};
+        this.keys = [].concat(this.options.keys || {
+            domainName: options.domainName,
+            keySelector: options.keySelector,
+            privateKey: options.privateKey
+        });
+    }
+
+    sign(input, extraOptions) {
+        let output = new PassThrough();
+        let inputStream = input;
+        let writeValue = false;
+
+        if (Buffer.isBuffer(input)) {
+            writeValue = input;
+            inputStream = new PassThrough();
+        } else if (typeof input === 'string') {
+            writeValue = Buffer.from(input);
+            inputStream = new PassThrough();
+        }
+
+        let options = this.options;
+        if (extraOptions && Object.keys(extraOptions).length) {
+            options = {};
+            Object.keys(this.options || {}).forEach(key => {
+                options[key] = this.options[key];
+            });
+            Object.keys(extraOptions || {}).forEach(key => {
+                if (!(key in options)) {
+                    options[key] = extraOptions[key];
+                }
+            });
+        }
+
+        let signer = new DKIMSigner(options, this.keys, inputStream, output);
+        setImmediate(() => {
+            signer.signStream();
+            if (writeValue) {
+                setImmediate(() => {
+                    inputStream.end(writeValue);
+                });
+            }
+        });
+
+        return output;
+    }
+}
+
+module.exports = DKIM;
diff --git a/node_modules/nodemailer/lib/dkim/message-parser.js b/node_modules/nodemailer/lib/dkim/message-parser.js
new file mode 100644
index 0000000..40d1971
--- /dev/null
+++ b/node_modules/nodemailer/lib/dkim/message-parser.js
@@ -0,0 +1,153 @@
+'use strict';
+
+const Transform = require('stream').Transform;
+
+/**
+ * MessageParser instance is a transform stream that separates message headers
+ * from the rest of the body. Headers are emitted with the 'headers' event. Message
+ * body is passed on as the resulting stream.
+ */
+class MessageParser extends Transform {
+    constructor(options) {
+        super(options);
+        this.lastBytes = Buffer.alloc(4);
+        this.headersParsed = false;
+        this.headerBytes = 0;
+        this.headerChunks = [];
+        this.rawHeaders = false;
+        this.bodySize = 0;
+    }
+
+    /**
+     * Keeps count of the last 4 bytes in order to detect line breaks on chunk boundaries
+     *
+     * @param {Buffer} data Next data chunk from the stream
+     */
+    updateLastBytes(data) {
+        let lblen = this.lastBytes.length;
+        let nblen = Math.min(data.length, lblen);
+
+        // shift existing bytes
+        for (let i = 0, len = lblen - nblen; i < len; i++) {
+            this.lastBytes[i] = this.lastBytes[i + nblen];
+        }
+
+        // add new bytes
+        for (let i = 1; i <= nblen; i++) {
+            this.lastBytes[lblen - i] = data[data.length - i];
+        }
+    }
+
+    /**
+     * Finds and removes message headers from the remaining body. We want to keep
+     * headers separated until final delivery to be able to modify these
+     *
+     * @param {Buffer} data Next chunk of data
+     * @return {Boolean} Returns true if headers are already found or false otherwise
+     */
+    checkHeaders(data) {
+        if (this.headersParsed) {
+            return true;
+        }
+
+        let lblen = this.lastBytes.length;
+        let headerPos = 0;
+        this.curLinePos = 0;
+        for (let i = 0, len = this.lastBytes.length + data.length; i < len; i++) {
+            let chr;
+            if (i < lblen) {
+                chr = this.lastBytes[i];
+            } else {
+                chr = data[i - lblen];
+            }
+            if (chr === 0x0A && i) {
+                let pr1 = i - 1 < lblen ? this.lastBytes[i - 1] : data[i - 1 - lblen];
+                let pr2 = i > 1 ? (i - 2 < lblen ? this.lastBytes[i - 2] : data[i - 2 - lblen]) : false;
+                if (pr1 === 0x0A) {
+                    this.headersParsed = true;
+                    headerPos = i - lblen + 1;
+                    this.headerBytes += headerPos;
+                    break;
+                } else if (pr1 === 0x0D && pr2 === 0x0A) {
+                    this.headersParsed = true;
+                    headerPos = i - lblen + 1;
+                    this.headerBytes += headerPos;
+                    break;
+                }
+            }
+        }
+
+        if (this.headersParsed) {
+            this.headerChunks.push(data.slice(0, headerPos));
+            this.rawHeaders = Buffer.concat(this.headerChunks, this.headerBytes);
+            this.headerChunks = null;
+            this.emit('headers', this.parseHeaders());
+            if (data.length - 1 > headerPos) {
+                let chunk = data.slice(headerPos);
+                this.bodySize += chunk.length;
+                // this would be the first chunk of data sent downstream
+                setImmediate(() => this.push(chunk));
+            }
+            return false;
+        } else {
+            this.headerBytes += data.length;
+            this.headerChunks.push(data);
+        }
+
+        // store last 4 bytes to catch header break
+        this.updateLastBytes(data);
+
+        return false;
+    }
+
+    _transform(chunk, encoding, callback) {
+        if (!chunk || !chunk.length) {
+            return callback();
+        }
+
+        if (typeof chunk === 'string') {
+            chunk = new Buffer(chunk, encoding);
+        }
+
+        let headersFound;
+
+        try {
+            headersFound = this.checkHeaders(chunk);
+        } catch (E) {
+            return callback(E);
+        }
+
+        if (headersFound) {
+            this.bodySize += chunk.length;
+            this.push(chunk);
+        }
+
+        setImmediate(callback);
+    }
+
+    _flush(callback) {
+        if (this.headerChunks) {
+            let chunk = Buffer.concat(this.headerChunks, this.headerBytes);
+            this.bodySize += chunk.length;
+            this.push(chunk);
+            this.headerChunks = null;
+        }
+        callback();
+    }
+
+    parseHeaders() {
+        let lines = (this.rawHeaders || '').toString().split(/\r?\n/);
+        for (let i = lines.length - 1; i > 0; i--) {
+            if (/^\s/.test(lines[i])) {
+                lines[i - 1] += '\n' + lines[i];
+                lines.splice(i, 1);
+            }
+        }
+        return lines.filter(line => line.trim()).map(line => ({
+            key: line.substr(0, line.indexOf(':')).trim().toLowerCase(),
+            line
+        }));
+    }
+}
+
+module.exports = MessageParser;
diff --git a/node_modules/nodemailer/lib/dkim/relaxed-body.js b/node_modules/nodemailer/lib/dkim/relaxed-body.js
new file mode 100644
index 0000000..af43be1
--- /dev/null
+++ b/node_modules/nodemailer/lib/dkim/relaxed-body.js
@@ -0,0 +1,152 @@
+'use strict';
+
+// streams through a message body and calculates relaxed body hash
+
+const Transform = require('stream').Transform;
+const crypto = require('crypto');
+
+class RelaxedBody extends Transform {
+    constructor(options) {
+        super();
+        options = options || {};
+        this.chunkBuffer = [];
+        this.chunkBufferLen = 0;
+        this.bodyHash = crypto.createHash(options.hashAlgo || 'sha1');
+        this.remainder = '';
+        this.byteLength = 0;
+
+        this.debug = options.debug;
+        this._debugBody = options.debug ? [] : false;
+    }
+
+    updateHash(chunk) {
+        let bodyStr;
+
+        // find next remainder
+        let nextRemainder = '';
+
+
+        // This crux finds and removes the spaces from the last line and the newline characters after the last non-empty line
+        // If we get another chunk that does not match this description then we can restore the previously processed data
+        let state = 'file';
+        for (let i = chunk.length - 1; i >= 0; i--) {
+            let c = chunk[i];
+
+            if (state === 'file' && (c === 0x0A || c === 0x0D)) {
+                // do nothing, found \n or \r at the end of chunk, stil end of file
+            } else if (state === 'file' && (c === 0x09 || c === 0x20)) {
+                // switch to line ending mode, this is the last non-empty line
+                state = 'line';
+            } else if (state === 'line' && (c === 0x09 || c === 0x20)) {
+                // do nothing, found ' ' or \t at the end of line, keep processing the last non-empty line
+            } else if (state === 'file' || state === 'line') {
+                // non line/file ending character found, switch to body mode
+                state = 'body';
+                if (i === chunk.length - 1) {
+                    // final char is not part of line end or file end, so do nothing
+                    break;
+                }
+            }
+
+            if (i === 0) {
+                // reached to the beginning of the chunk, check if it is still about the ending
+                // and if the remainder also matches
+                if ((state === 'file' && (!this.remainder || /[\r\n]$/.test(this.remainder))) ||
+                    (state === 'line' && (!this.remainder || /[ \t]$/.test(this.remainder)))) {
+                    // keep everything
+                    this.remainder += chunk.toString('binary');
+                    return;
+                } else if (state === 'line' || state === 'file') {
+                    // process existing remainder as normal line but store the current chunk
+                    nextRemainder = chunk.toString('binary');
+                    chunk = false;
+                    break;
+                }
+            }
+
+            if (state !== 'body') {
+                continue;
+            }
+
+            // reached first non ending byte
+            nextRemainder = chunk.slice(i + 1).toString('binary');
+            chunk = chunk.slice(0, i + 1);
+            break;
+        }
+
+        let needsFixing = !!this.remainder;
+        if (chunk && !needsFixing) {
+            // check if we even need to change anything
+            for (let i = 0, len = chunk.length; i < len; i++) {
+                if (i && chunk[i] === 0x0A && chunk[i - 1] !== 0x0D) {
+                    // missing \r before \n
+                    needsFixing = true;
+                    break;
+                } else if (i && chunk[i] === 0x0D && chunk[i - 1] === 0x20) {
+                    // trailing WSP found
+                    needsFixing = true;
+                    break;
+                } else if (i && chunk[i] === 0x20 && chunk[i - 1] === 0x20) {
+                    // multiple spaces found, needs to be replaced with just one
+                    needsFixing = true;
+                    break;
+                } else if (chunk[i] === 0x09) {
+                    // TAB found, needs to be replaced with a space
+                    needsFixing = true;
+                    break;
+                }
+            }
+        }
+
+        if (needsFixing) {
+            bodyStr = this.remainder + (chunk ? chunk.toString('binary') : '');
+            this.remainder = nextRemainder;
+            bodyStr = bodyStr.replace(/\r?\n/g, '\n') // use js line endings
+                .replace(/[ \t]*$/mg, '') // remove line endings, rtrim
+                .replace(/[ \t]+/mg, ' ') // single spaces
+                .replace(/\n/g, '\r\n'); // restore rfc822 line endings
+            chunk = Buffer.from(bodyStr, 'binary');
+        } else if (nextRemainder) {
+            this.remainder = nextRemainder;
+        }
+
+        if (this.debug) {
+            this._debugBody.push(chunk);
+        }
+        this.bodyHash.update(chunk);
+    }
+
+    _transform(chunk, encoding, callback) {
+        if (!chunk || !chunk.length) {
+            return callback();
+        }
+
+        if (typeof chunk === 'string') {
+            chunk = new Buffer(chunk, encoding);
+        }
+
+        this.updateHash(chunk);
+
+        this.byteLength += chunk.length;
+        this.push(chunk);
+
+        callback();
+    }
+
+    _flush(callback) {
+        // generate final hash and emit it
+        if (/[\r\n]$/.test(this.remainder) && this.byteLength > 2) {
+            // add terminating line end
+            this.bodyHash.update(Buffer.from('\r\n'));
+        }
+        if (!this.byteLength) {
+            // emit empty line buffer to keep the stream flowing
+            this.push(Buffer.from('\r\n'));
+            // this.bodyHash.update(Buffer.from('\r\n'));
+        }
+        this.emit('hash', this.bodyHash.digest('base64'), this.debug ? Buffer.concat(this._debugBody) : false);
+        callback();
+    }
+}
+
+module.exports = RelaxedBody;
diff --git a/node_modules/nodemailer/lib/dkim/sign.js b/node_modules/nodemailer/lib/dkim/sign.js
new file mode 100644
index 0000000..18be6d0
--- /dev/null
+++ b/node_modules/nodemailer/lib/dkim/sign.js
@@ -0,0 +1,105 @@
+'use strict';
+
+const punycode = require('punycode');
+const mimeFuncs = require('../mime-funcs');
+const crypto = require('crypto');
+
+/**
+ * Returns DKIM signature header line
+ *
+ * @param {Object} headers Parsed headers object from MessageParser
+ * @param {String} bodyHash Base64 encoded hash of the message
+ * @param {Object} options DKIM options
+ * @param {String} options.domainName Domain name to be signed for
+ * @param {String} options.keySelector DKIM key selector to use
+ * @param {String} options.privateKey DKIM private key to use
+ * @return {String} Complete header line
+ */
+
+module.exports = (headers, hashAlgo, bodyHash, options) => {
+    options = options || {};
+
+    // all listed fields from RFC4871 #5.5
+    let defaultFieldNames = 'From:Sender:Reply-To:Subject:Date:Message-ID:To:' +
+        'Cc:MIME-Version:Content-Type:Content-Transfer-Encoding:Content-ID:' +
+        'Content-Description:Resent-Date:Resent-From:Resent-Sender:' +
+        'Resent-To:Resent-Cc:Resent-Message-ID:In-Reply-To:References:' +
+        'List-Id:List-Help:List-Unsubscribe:List-Subscribe:List-Post:' +
+        'List-Owner:List-Archive';
+
+    let fieldNames = options.headerFieldNames || defaultFieldNames;
+
+    let canonicalizedHeaderData = relaxedHeaders(headers, fieldNames, options.skipFields);
+    let dkimHeader = generateDKIMHeader(options.domainName, options.keySelector, canonicalizedHeaderData.fieldNames, hashAlgo, bodyHash);
+
+    let signer, signature;
+
+    canonicalizedHeaderData.headers += 'dkim-signature:' + relaxedHeaderLine(dkimHeader);
+
+    signer = crypto.createSign(('rsa-' + hashAlgo).toUpperCase());
+    signer.update(canonicalizedHeaderData.headers);
+    try {
+        signature = signer.sign(options.privateKey, 'base64');
+    } catch (E) {
+        return false;
+    }
+
+    return dkimHeader + signature.replace(/(^.{73}|.{75}(?!\r?\n|\r))/g, '$&\r\n ').trim();
+};
+
+module.exports.relaxedHeaders = relaxedHeaders;
+
+function generateDKIMHeader(domainName, keySelector, fieldNames, hashAlgo, bodyHash) {
+    let dkim = [
+        'v=1',
+        'a=rsa-' + hashAlgo,
+        'c=relaxed/relaxed',
+        'd=' + punycode.toASCII(domainName),
+        'q=dns/txt',
+        's=' + keySelector,
+        'bh=' + bodyHash,
+        'h=' + fieldNames
+    ].join('; ');
+
+    return mimeFuncs.foldLines('DKIM-Signature: ' + dkim, 76) + ';\r\n b=';
+}
+
+function relaxedHeaders(headers, fieldNames, skipFields) {
+    let includedFields = new Set();
+    let skip = new Set();
+    let headerFields = new Map();
+
+    (skipFields || '').toLowerCase().split(':').forEach(field => {
+        skip.add(field.trim());
+    });
+
+    (fieldNames || '').toLowerCase().split(':').filter(field => !skip.has(field.trim())).forEach(field => {
+        includedFields.add(field.trim());
+    });
+
+    for (let i = headers.length - 1; i >= 0; i--) {
+        let line = headers[i];
+        // only include the first value from bottom to top
+        if (includedFields.has(line.key) && !headerFields.has(line.key)) {
+            headerFields.set(line.key, relaxedHeaderLine(line.line));
+        }
+    }
+
+    let headersList = [];
+    let fields = [];
+    includedFields.forEach(field => {
+        if (headerFields.has(field)) {
+            fields.push(field);
+            headersList.push(field + ':' + headerFields.get(field));
+        }
+    });
+
+    return {
+        headers: headersList.join('\r\n') + '\r\n',
+        fieldNames: fields.join(':')
+    };
+}
+
+function relaxedHeaderLine(line) {
+    return line.substr(line.indexOf(':') + 1).replace(/\r?\n/g, '').replace(/\s+/g, ' ').trim();
+}
diff --git a/node_modules/nodemailer/lib/fetch/cookies.js b/node_modules/nodemailer/lib/fetch/cookies.js
new file mode 100644
index 0000000..5a22e86
--- /dev/null
+++ b/node_modules/nodemailer/lib/fetch/cookies.js
@@ -0,0 +1,276 @@
+'use strict';
+
+// module to handle cookies
+
+const urllib = require('url');
+
+const SESSION_TIMEOUT = 1800; // 30 min
+
+/**
+ * Creates a biskviit cookie jar for managing cookie values in memory
+ *
+ * @constructor
+ * @param {Object} [options] Optional options object
+ */
+class Cookies {
+    constructor(options) {
+        this.options = options || {};
+        this.cookies = [];
+    }
+
+    /**
+     * Stores a cookie string to the cookie storage
+     *
+     * @param {String} cookieStr Value from the 'Set-Cookie:' header
+     * @param {String} url Current URL
+     */
+    set(cookieStr, url) {
+        let urlparts = urllib.parse(url || '');
+        let cookie = this.parse(cookieStr);
+        let domain;
+
+        if (cookie.domain) {
+            domain = cookie.domain.replace(/^\./, '');
+
+            // do not allow cross origin cookies
+            if (
+                // can't be valid if the requested domain is shorter than current hostname
+                urlparts.hostname.length < domain.length ||
+
+                // prefix domains with dot to be sure that partial matches are not used
+                ('.' + urlparts.hostname).substr(-domain.length + 1) !== ('.' + domain)) {
+                cookie.domain = urlparts.hostname;
+            }
+        } else {
+            cookie.domain = urlparts.hostname;
+        }
+
+        if (!cookie.path) {
+            cookie.path = this.getPath(urlparts.pathname);
+        }
+
+        // if no expire date, then use sessionTimeout value
+        if (!cookie.expires) {
+            cookie.expires = new Date(Date.now() + (Number(this.options.sessionTimeout || SESSION_TIMEOUT) || SESSION_TIMEOUT) * 1000);
+        }
+
+        return this.add(cookie);
+    }
+
+    /**
+     * Returns cookie string for the 'Cookie:' header.
+     *
+     * @param {String} url URL to check for
+     * @returns {String} Cookie header or empty string if no matches were found
+     */
+    get(url) {
+        return this.list(url).map(cookie =>
+            cookie.name + '=' + cookie.value).join('; ');
+    }
+
+    /**
+     * Lists all valied cookie objects for the specified URL
+     *
+     * @param {String} url URL to check for
+     * @returns {Array} An array of cookie objects
+     */
+    list(url) {
+        let result = [];
+        let i;
+        let cookie;
+
+        for (i = this.cookies.length - 1; i >= 0; i--) {
+            cookie = this.cookies[i];
+
+            if (this.isExpired(cookie)) {
+                this.cookies.splice(i, i);
+                continue;
+            }
+
+            if (this.match(cookie, url)) {
+                result.unshift(cookie);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Parses cookie string from the 'Set-Cookie:' header
+     *
+     * @param {String} cookieStr String from the 'Set-Cookie:' header
+     * @returns {Object} Cookie object
+     */
+    parse(cookieStr) {
+        let cookie = {};
+
+        (cookieStr || '').toString().split(';').forEach(cookiePart => {
+            let valueParts = cookiePart.split('=');
+            let key = valueParts.shift().trim().toLowerCase();
+            let value = valueParts.join('=').trim();
+            let domain;
+
+            if (!key) {
+                // skip empty parts
+                return;
+            }
+
+            switch (key) {
+
+                case 'expires':
+                    value = new Date(value);
+                    // ignore date if can not parse it
+                    if (value.toString() !== 'Invalid Date') {
+                        cookie.expires = value;
+                    }
+                    break;
+
+                case 'path':
+                    cookie.path = value;
+                    break;
+
+                case 'domain':
+                    domain = value.toLowerCase();
+                    if (domain.length && domain.charAt(0) !== '.') {
+                        domain = '.' + domain; // ensure preceeding dot for user set domains
+                    }
+                    cookie.domain = domain;
+                    break;
+
+                case 'max-age':
+                    cookie.expires = new Date(Date.now() + (Number(value) || 0) * 1000);
+                    break;
+
+                case 'secure':
+                    cookie.secure = true;
+                    break;
+
+                case 'httponly':
+                    cookie.httponly = true;
+                    break;
+
+                default:
+                    if (!cookie.name) {
+                        cookie.name = key;
+                        cookie.value = value;
+                    }
+            }
+        });
+
+        return cookie;
+    }
+
+    /**
+     * Checks if a cookie object is valid for a specified URL
+     *
+     * @param {Object} cookie Cookie object
+     * @param {String} url URL to check for
+     * @returns {Boolean} true if cookie is valid for specifiec URL
+     */
+    match(cookie, url) {
+        let urlparts = urllib.parse(url || '');
+
+        // check if hostname matches
+        // .foo.com also matches subdomains, foo.com does not
+        if (urlparts.hostname !== cookie.domain && (cookie.domain.charAt(0) !== '.' || ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
+            return false;
+        }
+
+        // check if path matches
+        let path = this.getPath(urlparts.pathname);
+        if (path.substr(0, cookie.path.length) !== cookie.path) {
+            return false;
+        }
+
+        // check secure argument
+        if (cookie.secure && urlparts.protocol !== 'https:') {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Adds (or updates/removes if needed) a cookie object to the cookie storage
+     *
+     * @param {Object} cookie Cookie value to be stored
+     */
+    add(cookie) {
+        let i;
+        let len;
+
+        // nothing to do here
+        if (!cookie || !cookie.name) {
+            return false;
+        }
+
+        // overwrite if has same params
+        for (i = 0, len = this.cookies.length; i < len; i++) {
+            if (this.compare(this.cookies[i], cookie)) {
+
+                // check if the cookie needs to be removed instead
+                if (this.isExpired(cookie)) {
+                    this.cookies.splice(i, 1); // remove expired/unset cookie
+                    return false;
+                }
+
+                this.cookies[i] = cookie;
+                return true;
+            }
+        }
+
+        // add as new if not already expired
+        if (!this.isExpired(cookie)) {
+            this.cookies.push(cookie);
+        }
+
+        return true;
+    }
+
+    /**
+     * Checks if two cookie objects are the same
+     *
+     * @param {Object} a Cookie to check against
+     * @param {Object} b Cookie to check against
+     * @returns {Boolean} True, if the cookies are the same
+     */
+    compare(a, b) {
+        return a.name === b.name && a.path === b.path && a.domain === b.domain && a.secure === b.secure && a.httponly === a.httponly;
+    }
+
+    /**
+     * Checks if a cookie is expired
+     *
+     * @param {Object} cookie Cookie object to check against
+     * @returns {Boolean} True, if the cookie is expired
+     */
+    isExpired(cookie) {
+        return (cookie.expires && cookie.expires < new Date()) || !cookie.value;
+    }
+
+    /**
+     * Returns normalized cookie path for an URL path argument
+     *
+     * @param {String} pathname
+     * @returns {String} Normalized path
+     */
+    getPath(pathname) {
+        let path = (pathname || '/').split('/');
+        path.pop(); // remove filename part
+        path = path.join('/').trim();
+
+        // ensure path prefix /
+        if (path.charAt(0) !== '/') {
+            path = '/' + path;
+        }
+
+        // ensure path suffix /
+        if (path.substr(-1) !== '/') {
+            path += '/';
+        }
+
+        return path;
+    }
+}
+
+module.exports = Cookies;
diff --git a/node_modules/nodemailer/lib/fetch/index.js b/node_modules/nodemailer/lib/fetch/index.js
new file mode 100644
index 0000000..5a1216d
--- /dev/null
+++ b/node_modules/nodemailer/lib/fetch/index.js
@@ -0,0 +1,249 @@
+'use strict';
+
+const http = require('http');
+const https = require('https');
+const urllib = require('url');
+const zlib = require('zlib');
+const PassThrough = require('stream').PassThrough;
+const Cookies = require('./cookies');
+const packageData = require('../../package.json');
+
+const MAX_REDIRECTS = 5;
+
+module.exports = function (url, options) {
+    return fetch(url, options);
+};
+
+module.exports.Cookies = Cookies;
+
+function fetch(url, options) {
+    options = options || {};
+
+    options.fetchRes = options.fetchRes || new PassThrough();
+    options.cookies = options.cookies || new Cookies();
+    options.redirects = options.redirects || 0;
+    options.maxRedirects = isNaN(options.maxRedirects) ? MAX_REDIRECTS : options.maxRedirects;
+
+    if (options.cookie) {
+        [].concat(options.cookie || []).forEach(cookie => {
+            options.cookies.set(cookie, url);
+        });
+        options.cookie = false;
+    }
+
+    let fetchRes = options.fetchRes;
+    let parsed = urllib.parse(url);
+    let method = (options.method || '').toString().trim().toUpperCase() || 'GET';
+    let finished = false;
+    let cookies;
+    let body;
+
+    let handler = parsed.protocol === 'https:' ? https : http;
+
+    let headers = {
+        'accept-encoding': 'gzip,deflate',
+        'user-agent': 'nodemailer/' + packageData.version
+    };
+
+    Object.keys(options.headers || {}).forEach(key => {
+        headers[key.toLowerCase().trim()] = options.headers[key];
+    });
+
+    if (options.userAgent) {
+        headers['user-agent'] = options.userAgent;
+    }
+
+    if (parsed.auth) {
+        headers.Authorization = 'Basic ' + new Buffer(parsed.auth).toString('base64');
+    }
+
+    if ((cookies = options.cookies.get(url))) {
+        headers.cookie = cookies;
+    }
+
+    if (options.body) {
+        if (options.contentType !== false) {
+            headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded';
+        }
+
+        if (typeof options.body.pipe === 'function') {
+            // it's a stream
+            headers['Transfer-Encoding'] = 'chunked';
+            body = options.body;
+            body.on('error', err => {
+                if (finished) {
+                    return;
+                }
+                finished = true;
+                err.type = 'FETCH';
+                err.sourceUrl = url;
+                fetchRes.emit('error', err);
+            });
+        } else {
+            if (options.body instanceof Buffer) {
+                body = options.body;
+            } else if (typeof options.body === 'object') {
+                body = new Buffer(Object.keys(options.body).map(key => {
+                    let value = options.body[key].toString().trim();
+                    return encodeURIComponent(key) + '=' + encodeURIComponent(value);
+                }).join('&'));
+            } else {
+                body = new Buffer(options.body.toString().trim());
+            }
+
+            headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded';
+            headers['Content-Length'] = body.length;
+        }
+        // if method is not provided, use POST instead of GET
+        method = (options.method || '').toString().trim().toUpperCase() || 'POST';
+    }
+
+    let req;
+    let reqOptions = {
+        method,
+        host: parsed.hostname,
+        path: parsed.path,
+        port: parsed.port ? parsed.port : (parsed.protocol === 'https:' ? 443 : 80),
+        headers,
+        rejectUnauthorized: false,
+        agent: false
+    };
+
+    if (options.tls) {
+        Object.keys(options.tls).forEach(key => {
+            reqOptions[key] = options.tls[key];
+        });
+    }
+
+    try {
+        req = handler.request(reqOptions);
+    } catch (E) {
+        finished = true;
+        setImmediate(() => {
+            E.type = 'FETCH';
+            E.sourceUrl = url;
+            fetchRes.emit('error', E);
+        });
+        return fetchRes;
+    }
+
+    if (options.timeout) {
+        req.setTimeout(options.timeout, () => {
+            if (finished) {
+                return;
+            }
+            finished = true;
+            req.abort();
+            let err = new Error('Request Timeout');
+            err.type = 'FETCH';
+            err.sourceUrl = url;
+            fetchRes.emit('error', err);
+        });
+    }
+
+    req.on('error', err => {
+        if (finished) {
+            return;
+        }
+        finished = true;
+        err.type = 'FETCH';
+        err.sourceUrl = url;
+        fetchRes.emit('error', err);
+    });
+
+    req.on('response', res => {
+        let inflate;
+
+        if (finished) {
+            return;
+        }
+
+        switch (res.headers['content-encoding']) {
+            case 'gzip':
+            case 'deflate':
+                inflate = zlib.createUnzip();
+                break;
+        }
+
+        if (res.headers['set-cookie']) {
+            [].concat(res.headers['set-cookie'] || []).forEach(cookie => {
+                options.cookies.set(cookie, url);
+            });
+        }
+
+        if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
+            // redirect
+            options.redirects++;
+            if (options.redirects > options.maxRedirects) {
+                finished = true;
+                let err = new Error('Maximum redirect count exceeded');
+                err.type = 'FETCH';
+                err.sourceUrl = url;
+                fetchRes.emit('error', err);
+                req.abort();
+                return;
+            }
+            return fetch(urllib.resolve(url, res.headers.location), options);
+        }
+
+        fetchRes.statusCode = res.statusCode;
+
+        if (res.statusCode >= 300 && !options.allowErrorResponse) {
+            finished = true;
+            let err = new Error('Invalid status code ' + res.statusCode);
+            err.type = 'FETCH';
+            err.sourceUrl = url;
+            fetchRes.emit('error', err);
+            req.abort();
+            return;
+        }
+
+        res.on('error', err => {
+            if (finished) {
+                return;
+            }
+            finished = true;
+            err.type = 'FETCH';
+            err.sourceUrl = url;
+            fetchRes.emit('error', err);
+            req.abort();
+        });
+
+        if (inflate) {
+            res.pipe(inflate).pipe(fetchRes);
+            inflate.on('error', err => {
+                if (finished) {
+                    return;
+                }
+                finished = true;
+                err.type = 'FETCH';
+                err.sourceUrl = url;
+                fetchRes.emit('error', err);
+                req.abort();
+            });
+        } else {
+            res.pipe(fetchRes);
+        }
+    });
+
+    setImmediate(() => {
+        if (body) {
+            try {
+                if (typeof body.pipe === 'function') {
+                    return body.pipe(req);
+                } else {
+                    req.write(body);
+                }
+            } catch (err) {
+                finished = true;
+                err.type = 'FETCH';
+                err.sourceUrl = url;
+                fetchRes.emit('error', err);
+                return;
+            }
+        }
+        req.end();
+    });
+
+    return fetchRes;
+}
diff --git a/node_modules/nodemailer/lib/json-transport/index.js b/node_modules/nodemailer/lib/json-transport/index.js
new file mode 100644
index 0000000..d35edcf
--- /dev/null
+++ b/node_modules/nodemailer/lib/json-transport/index.js
@@ -0,0 +1,110 @@
+'use strict';
+
+const packageData = require('../../package.json');
+const shared = require('../shared');
+
+/**
+ * Generates a Transport object for Sendmail
+ *
+ * Possible options can be the following:
+ *
+ *  * **path** optional path to sendmail binary
+ *  * **newline** either 'windows' or 'unix'
+ *  * **args** an array of arguments for the sendmail binary
+ *
+ * @constructor
+ * @param {Object} optional config parameter for the AWS Sendmail service
+ */
+class JSONTransport {
+    constructor(options) {
+        options = options || {};
+
+        this.options = options || {};
+
+        this.name = 'StreamTransport';
+        this.version = packageData.version;
+
+        this.logger = shared.getLogger(this.options, {
+            component: this.options.component || 'stream-transport'
+        });
+    }
+
+    /**
+     * <p>Compiles a mailcomposer message and forwards it to handler that sends it.</p>
+     *
+     * @param {Object} emailMessage MailComposer object
+     * @param {Function} callback Callback function to run when the sending is completed
+     */
+    send(mail, done) {
+        // Sendmail strips this header line by itself
+        mail.message.keepBcc = true;
+
+        let envelope = mail.data.envelope || mail.message.getEnvelope();
+        let messageId = mail.message.messageId();
+
+        let recipients = [].concat(envelope.to || []);
+        if (recipients.length > 3) {
+            recipients.push('...and ' + recipients.splice(2).length + ' more');
+        }
+        this.logger.info({
+            tnx: 'send',
+            messageId
+        }, 'Composing JSON structure of %s to <%s>', messageId, recipients.join(', '));
+
+        setImmediate(() => {
+            mail.resolveAll((err, data) => {
+                if (err) {
+                    this.logger.error({
+                        err,
+                        tnx: 'send',
+                        messageId
+                    }, 'Failed building JSON structure for %s. %s', messageId, err.message);
+                    return done(err);
+                }
+
+                data.messageId = messageId;
+
+                ['html', 'text', 'watchHtml'].forEach(key => {
+                    if (data[key] && data[key].content) {
+                        if (typeof data[key].content === 'string') {
+                            data[key] = data[key].content;
+                        } else if (Buffer.isBuffer(data[key].content)) {
+                            data[key] = data[key].content.toString();
+                        }
+                    }
+                });
+
+                if (data.icalEvent && Buffer.isBuffer(data.icalEvent.content)) {
+                    data.icalEvent.content = data.icalEvent.content.toString('base64');
+                    data.icalEvent.encoding = 'base64';
+                }
+
+                if (data.alternatives && data.alternatives.length) {
+                    data.alternatives.forEach(alternative => {
+                        if (alternative && alternative.content && Buffer.isBuffer(alternative.content)) {
+                            alternative.content = alternative.content.toString('base64');
+                            alternative.encoding = 'base64';
+                        }
+                    });
+                }
+
+                if (data.attachments && data.attachments.length) {
+                    data.attachments.forEach(attachment => {
+                        if (attachment && attachment.content && Buffer.isBuffer(attachment.content)) {
+                            attachment.content = attachment.content.toString('base64');
+                            attachment.encoding = 'base64';
+                        }
+                    });
+                }
+
+                return done(null, {
+                    envelope: mail.data.envelope || mail.message.getEnvelope(),
+                    messageId,
+                    message: JSON.stringify(data)
+                });
+            });
+        });
+    }
+}
+
+module.exports = JSONTransport;
diff --git a/node_modules/nodemailer/lib/mail-composer/index.js b/node_modules/nodemailer/lib/mail-composer/index.js
new file mode 100644
index 0000000..db514e4
--- /dev/null
+++ b/node_modules/nodemailer/lib/mail-composer/index.js
@@ -0,0 +1,520 @@
+/* eslint no-undefined: 0 */
+
+'use strict';
+
+const MimeNode = require('../mime-node');
+const mimeFuncs = require('../mime-funcs');
+
+/**
+ * Creates the object for composing a MimeNode instance out from the mail options
+ *
+ * @constructor
+ * @param {Object} mail Mail options
+ */
+class MailComposer {
+    constructor(mail) {
+        this.mail = mail || {};
+        this.message = false;
+    }
+
+    /**
+     * Builds MimeNode instance
+     */
+    compile() {
+        this._alternatives = this.getAlternatives();
+        this._htmlNode = this._alternatives.filter(alternative => /^text\/html\b/i.test(alternative.contentType)).pop();
+        this._attachments = this.getAttachments(!!this._htmlNode);
+
+        this._useRelated = !!(this._htmlNode && this._attachments.related.length);
+        this._useAlternative = this._alternatives.length > 1;
+        this._useMixed = this._attachments.attached.length > 1 || (this._alternatives.length && this._attachments.attached.length === 1);
+
+        // Compose MIME tree
+        if (this.mail.raw) {
+            this.message = new MimeNode().setRaw(this.mail.raw);
+        } else if (this._useMixed) {
+            this.message = this._createMixed();
+        } else if (this._useAlternative) {
+            this.message = this._createAlternative();
+        } else if (this._useRelated) {
+            this.message = this._createRelated();
+        } else {
+            this.message = this._createContentNode(false, [].concat(this._alternatives || []).concat(this._attachments.attached || []).shift() || {
+                contentType: 'text/plain',
+                content: ''
+            });
+        }
+
+        // Add custom headers
+        if (this.mail.headers) {
+            this.message.addHeader(this.mail.headers);
+        }
+
+        // Add headers to the root node, always overrides custom headers
+        [
+            'from',
+            'sender',
+            'to',
+            'cc',
+            'bcc',
+            'reply-to',
+            'in-reply-to',
+            'references',
+            'subject',
+            'message-id',
+            'date'
+        ].forEach(header => {
+            let key = header.replace(/-(\w)/g, (o, c) => c.toUpperCase());
+            if (this.mail[key]) {
+                this.message.setHeader(header, this.mail[key]);
+            }
+        });
+
+        // Sets custom envelope
+        if (this.mail.envelope) {
+            this.message.setEnvelope(this.mail.envelope);
+        }
+
+        // ensure Message-Id value
+        this.message.messageId();
+
+        return this.message;
+    }
+
+    /**
+     * List all attachments. Resulting attachment objects can be used as input for MimeNode nodes
+     *
+     * @param {Boolean} findRelated If true separate related attachments from attached ones
+     * @returns {Object} An object of arrays (`related` and `attached`)
+     */
+    getAttachments(findRelated) {
+        let icalEvent, eventObject;
+        let attachments = [].concat(this.mail.attachments || []).map((attachment, i) => {
+            let data;
+            let isMessageNode = /^message\//i.test(attachment.contentType);
+
+            if (/^data:/i.test(attachment.path || attachment.href)) {
+                attachment = this._processDataUrl(attachment);
+            }
+
+            data = {
+                contentType: attachment.contentType ||
+                    mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin'),
+                contentDisposition: attachment.contentDisposition || (isMessageNode ? 'inline' : 'attachment'),
+                contentTransferEncoding: attachment.contentTransferEncoding
+            };
+
+            if (attachment.filename) {
+                data.filename = attachment.filename;
+            } else if (!isMessageNode && attachment.filename !== false) {
+                data.filename = (attachment.path || attachment.href || '').split('/').pop() || 'attachment-' + (i + 1);
+                if (data.filename.indexOf('.') < 0) {
+                    data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
+                }
+            }
+
+            if (/^https?:\/\//i.test(attachment.path)) {
+                attachment.href = attachment.path;
+                attachment.path = undefined;
+            }
+
+            if (attachment.cid) {
+                data.cid = attachment.cid;
+            }
+
+            if (attachment.raw) {
+                data.raw = attachment.raw;
+            } else if (attachment.path) {
+                data.content = {
+                    path: attachment.path
+                };
+            } else if (attachment.href) {
+                data.content = {
+                    href: attachment.href
+                };
+            } else {
+                data.content = attachment.content || '';
+            }
+
+            if (attachment.encoding) {
+                data.encoding = attachment.encoding;
+            }
+
+            if (attachment.headers) {
+                data.headers = attachment.headers;
+            }
+
+            return data;
+        });
+
+        if (this.mail.icalEvent) {
+            if (typeof this.mail.icalEvent === 'object' && (this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw)) {
+                icalEvent = this.mail.icalEvent;
+            } else {
+                icalEvent = {
+                    content: this.mail.icalEvent
+                };
+            }
+
+            eventObject = {};
+            Object.keys(icalEvent).forEach(key => {
+                eventObject[key] = icalEvent[key];
+            });
+
+            eventObject.contentType = 'application/ics';
+            if (!eventObject.headers) {
+                eventObject.headers = {};
+            }
+            eventObject.filename = eventObject.filename || 'invite.ics';
+            eventObject.headers['Content-Disposition'] = 'attachment';
+            eventObject.headers['Content-Transfer-Encoding'] = 'base64';
+        }
+
+        if (!findRelated) {
+            return {
+                attached: attachments.concat(eventObject || []),
+                related: []
+            };
+        } else {
+            return {
+                attached: attachments.filter(attachment => !attachment.cid).concat(eventObject || []),
+                related: attachments.filter(attachment => !!attachment.cid)
+            };
+        }
+    }
+
+    /**
+     * List alternatives. Resulting objects can be used as input for MimeNode nodes
+     *
+     * @returns {Array} An array of alternative elements. Includes the `text` and `html` values as well
+     */
+    getAlternatives() {
+        let alternatives = [],
+            text, html, watchHtml, icalEvent, eventObject;
+
+        if (this.mail.text) {
+            if (typeof this.mail.text === 'object' && (this.mail.text.content || this.mail.text.path || this.mail.text.href || this.mail.text.raw)) {
+                text = this.mail.text;
+            } else {
+                text = {
+                    content: this.mail.text
+                };
+            }
+            text.contentType = 'text/plain' + (!text.encoding && mimeFuncs.isPlainText(text.content) ? '' : '; charset=utf-8');
+        }
+
+        if (this.mail.watchHtml) {
+            if (typeof this.mail.watchHtml === 'object' && (this.mail.watchHtml.content || this.mail.watchHtml.path || this.mail.watchHtml.href || this.mail.watchHtml.raw)) {
+                watchHtml = this.mail.watchHtml;
+            } else {
+                watchHtml = {
+                    content: this.mail.watchHtml
+                };
+            }
+            watchHtml.contentType = 'text/watch-html' + (!watchHtml.encoding && mimeFuncs.isPlainText(watchHtml.content) ? '' : '; charset=utf-8');
+        }
+
+        // only include the calendar alternative if there are no attachments
+        // otherwise you might end up in a blank screen on some clients
+        if (this.mail.icalEvent && !(this.mail.attachments && this.mail.attachments.length)) {
+            if (typeof this.mail.icalEvent === 'object' && (this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw)) {
+                icalEvent = this.mail.icalEvent;
+            } else {
+                icalEvent = {
+                    content: this.mail.icalEvent
+                };
+            }
+
+            eventObject = {};
+            Object.keys(icalEvent).forEach(key => {
+                eventObject[key] = icalEvent[key];
+            });
+
+            if (eventObject.content && typeof eventObject.content === 'object') {
+                // we are going to have the same attachment twice, so mark this to be
+                // resolved just once
+                eventObject.content._resolve = true;
+            }
+
+            eventObject.filename = false;
+            eventObject.contentType = 'text/calendar; charset="utf-8"; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
+            if (!eventObject.headers) {
+                eventObject.headers = {};
+            }
+        }
+
+        if (this.mail.html) {
+            if (typeof this.mail.html === 'object' && (this.mail.html.content || this.mail.html.path || this.mail.html.href || this.mail.html.raw)) {
+                html = this.mail.html;
+            } else {
+                html = {
+                    content: this.mail.html
+                };
+            }
+            html.contentType = 'text/html' + (!html.encoding && mimeFuncs.isPlainText(html.content) ? '' : '; charset=utf-8');
+        }
+
+        [].
+        concat(text || []).
+        concat(watchHtml || []).
+        concat(html || []).
+        concat(eventObject || []).
+        concat(this.mail.alternatives || []).
+        forEach(alternative => {
+            let data;
+
+            if (/^data:/i.test(alternative.path || alternative.href)) {
+                alternative = this._processDataUrl(alternative);
+            }
+
+            data = {
+                contentType: alternative.contentType ||
+                    mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
+                contentTransferEncoding: alternative.contentTransferEncoding
+            };
+
+            if (alternative.filename) {
+                data.filename = alternative.filename;
+            }
+
+            if (/^https?:\/\//i.test(alternative.path)) {
+                alternative.href = alternative.path;
+                alternative.path = undefined;
+            }
+
+            if (alternative.raw) {
+                data.raw = alternative.raw;
+            } else if (alternative.path) {
+                data.content = {
+                    path: alternative.path
+                };
+            } else if (alternative.href) {
+                data.content = {
+                    href: alternative.href
+                };
+            } else {
+                data.content = alternative.content || '';
+            }
+
+            if (alternative.encoding) {
+                data.encoding = alternative.encoding;
+            }
+
+            if (alternative.headers) {
+                data.headers = alternative.headers;
+            }
+
+            alternatives.push(data);
+        });
+
+        return alternatives;
+    }
+
+    /**
+     * Builds multipart/mixed node. It should always contain different type of elements on the same level
+     * eg. text + attachments
+     *
+     * @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
+     * @returns {Object} MimeNode node element
+     */
+    _createMixed(parentNode) {
+        let node;
+
+        if (!parentNode) {
+            node = new MimeNode('multipart/mixed', {
+                baseBoundary: this.mail.baseBoundary,
+                textEncoding: this.mail.textEncoding,
+                boundaryPrefix: this.mail.boundaryPrefix,
+                disableUrlAccess: this.mail.disableUrlAccess,
+                disableFileAccess: this.mail.disableFileAccess
+            });
+        } else {
+            node = parentNode.createChild('multipart/mixed', {
+                disableUrlAccess: this.mail.disableUrlAccess,
+                disableFileAccess: this.mail.disableFileAccess
+            });
+        }
+
+        if (this._useAlternative) {
+            this._createAlternative(node);
+        } else if (this._useRelated) {
+            this._createRelated(node);
+        }
+
+        [].concat(!this._useAlternative && this._alternatives || []).concat(this._attachments.attached || []).forEach(element => {
+            // if the element is a html node from related subpart then ignore it
+            if (!this._useRelated || element !== this._htmlNode) {
+                this._createContentNode(node, element);
+            }
+        });
+
+        return node;
+    }
+
+    /**
+     * Builds multipart/alternative node. It should always contain same type of elements on the same level
+     * eg. text + html view of the same data
+     *
+     * @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
+     * @returns {Object} MimeNode node element
+     */
+    _createAlternative(parentNode) {
+        let node;
+
+        if (!parentNode) {
+            node = new MimeNode('multipart/alternative', {
+                baseBoundary: this.mail.baseBoundary,
+                textEncoding: this.mail.textEncoding,
+                boundaryPrefix: this.mail.boundaryPrefix,
+                disableUrlAccess: this.mail.disableUrlAccess,
+                disableFileAccess: this.mail.disableFileAccess
+            });
+        } else {
+            node = parentNode.createChild('multipart/alternative', {
+                disableUrlAccess: this.mail.disableUrlAccess,
+                disableFileAccess: this.mail.disableFileAccess
+            });
+        }
+
+        this._alternatives.forEach(alternative => {
+            if (this._useRelated && this._htmlNode === alternative) {
+                this._createRelated(node);
+            } else {
+                this._createContentNode(node, alternative);
+            }
+        });
+
+        return node;
+    }
+
+    /**
+     * Builds multipart/related node. It should always contain html node with related attachments
+     *
+     * @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
+     * @returns {Object} MimeNode node element
+     */
+    _createRelated(parentNode) {
+        let node;
+
+        if (!parentNode) {
+            node = new MimeNode('multipart/related; type="text/html"', {
+                baseBoundary: this.mail.baseBoundary,
+                textEncoding: this.mail.textEncoding,
+                boundaryPrefix: this.mail.boundaryPrefix,
+                disableUrlAccess: this.mail.disableUrlAccess,
+                disableFileAccess: this.mail.disableFileAccess
+            });
+        } else {
+            node = parentNode.createChild('multipart/related; type="text/html"', {
+                disableUrlAccess: this.mail.disableUrlAccess,
+                disableFileAccess: this.mail.disableFileAccess
+            });
+        }
+
+        this._createContentNode(node, this._htmlNode);
+
+        this._attachments.related.forEach(alternative => this._createContentNode(node, alternative));
+
+        return node;
+    }
+
+    /**
+     * Creates a regular node with contents
+     *
+     * @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
+     * @param {Object} element Node data
+     * @returns {Object} MimeNode node element
+     */
+    _createContentNode(parentNode, element) {
+        element = element || {};
+        element.content = element.content || '';
+
+        let node;
+        let encoding = (element.encoding || 'utf8')
+            .toString()
+            .toLowerCase()
+            .replace(/[-_\s]/g, '');
+
+        if (!parentNode) {
+            node = new MimeNode(element.contentType, {
+                filename: element.filename,
+                baseBoundary: this.mail.baseBoundary,
+                textEncoding: this.mail.textEncoding,
+                boundaryPrefix: this.mail.boundaryPrefix,
+                disableUrlAccess: this.mail.disableUrlAccess,
+                disableFileAccess: this.mail.disableFileAccess
+            });
+        } else {
+            node = parentNode.createChild(element.contentType, {
+                filename: element.filename,
+                disableUrlAccess: this.mail.disableUrlAccess,
+                disableFileAccess: this.mail.disableFileAccess
+            });
+        }
+
+        // add custom headers
+        if (element.headers) {
+            node.addHeader(element.headers);
+        }
+
+        if (element.cid) {
+            node.setHeader('Content-Id', '<' + element.cid.replace(/[<>]/g, '') + '>');
+        }
+
+        if (element.contentTransferEncoding) {
+            node.setHeader('Content-Transfer-Encoding', element.contentTransferEncoding);
+        } else if (this.mail.encoding && /^text\//i.test(element.contentType)) {
+            node.setHeader('Content-Transfer-Encoding', this.mail.encoding);
+        }
+
+        if (!/^text\//i.test(element.contentType) || element.contentDisposition) {
+            node.setHeader('Content-Disposition', element.contentDisposition || (element.cid ? 'inline' : 'attachment'));
+        }
+
+        if (typeof element.content === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) {
+            element.content = new Buffer(element.content, encoding);
+        }
+
+        // prefer pregenerated raw content
+        if (element.raw) {
+            node.setRaw(element.raw);
+        } else {
+            node.setContent(element.content);
+        }
+
+        return node;
+    }
+
+    /**
+     * Parses data uri and converts it to a Buffer
+     *
+     * @param {Object} element Content element
+     * @return {Object} Parsed element
+     */
+    _processDataUrl(element) {
+        let parts = (element.path || element.href).match(/^data:((?:[^;]*;)*(?:[^,]*)),(.*)$/i);
+        if (!parts) {
+            return element;
+        }
+
+        element.content = /\bbase64$/i.test(parts[1]) ? new Buffer(parts[2], 'base64') : new Buffer(decodeURIComponent(parts[2]));
+
+        if ('path' in element) {
+            element.path = false;
+        }
+
+        if ('href' in element) {
+            element.href = false;
+        }
+
+        parts[1].split(';').forEach(item => {
+            if (/^\w+\/[^\/]+$/i.test(item)) {
+                element.contentType = element.contentType || item.toLowerCase();
+            }
+        });
+
+        return element;
+    }
+}
+
+module.exports = MailComposer;
diff --git a/node_modules/nodemailer/lib/mailer/index.js b/node_modules/nodemailer/lib/mailer/index.js
new file mode 100644
index 0000000..4121ca9
--- /dev/null
+++ b/node_modules/nodemailer/lib/mailer/index.js
@@ -0,0 +1,371 @@
+'use strict';
+
+const EventEmitter = require('events');
+const shared = require('../shared');
+const mimeTypes = require('../mime-funcs/mime-types');
+const MailComposer = require('../mail-composer');
+const DKIM = require('../dkim');
+const httpProxyClient = require('../smtp-connection/http-proxy-client');
+const util = require('util');
+const urllib = require('url');
+const packageData = require('../../package.json');
+const MailMessage = require('./mail-message');
+const net = require('net');
+const dns = require('dns');
+const crypto = require('crypto');
+
+/**
+ * Creates an object for exposing the Mail API
+ *
+ * @constructor
+ * @param {Object} transporter Transport object instance to pass the mails to
+ */
+class Mail extends EventEmitter {
+    constructor(transporter, options, defaults) {
+        super();
+
+        this.options = options || {};
+        this._defaults = defaults || {};
+
+        this._defaultPlugins = {
+            compile: [(...args) => this._convertDataImages(...args)],
+            stream: []
+        };
+
+        this._userPlugins = {
+            compile: [],
+            stream: []
+        };
+
+        this.meta = new Map();
+
+        this.dkim = this.options.dkim ? new DKIM(this.options.dkim) : false;
+
+        this.transporter = transporter;
+        this.transporter.mailer = this;
+
+        this.logger = shared.getLogger(this.options, {
+            component: this.options.component || 'mail'
+        });
+
+        this.logger.debug({
+            tnx: 'create'
+        }, 'Creating transport: %s', this.getVersionString());
+
+        // setup emit handlers for the transporter
+        if (typeof transporter.on === 'function') {
+
+            // deprecated log interface
+            this.transporter.on('log', log => {
+                this.logger.debug({
+                    tnx: 'transport'
+                }, '%s: %s', log.type, log.message);
+            });
+
+            // transporter errors
+            this.transporter.on('error', err => {
+                this.logger.error({
+                    err,
+                    tnx: 'transport'
+                }, 'Transport Error: %s', err.message);
+                this.emit('error', err);
+            });
+
+            // indicates if the sender has became idle
+            this.transporter.on('idle', (...args) => {
+                this.emit('idle', ...args);
+            });
+        }
+
+        /**
+         * Optional methods passed to the underlying transport object
+         */
+        ['close', 'isIdle', 'verify'].forEach(method => {
+            this[method] = (...args) => {
+                if (typeof this.transporter[method] === 'function') {
+                    return this.transporter[method](...args);
+                } else {
+                    this.logger.warn({
+                        tnx: 'transport',
+                        methodName: method
+                    }, 'Non existing method %s called for transport', method);
+                    return false;
+                }
+            };
+        });
+
+        // setup proxy handling
+        if (this.options.proxy && typeof this.options.proxy === 'string') {
+            this.setupProxy(this.options.proxy);
+        }
+    }
+
+    use(step, plugin) {
+        step = (step || '').toString();
+        if (!this._userPlugins.hasOwnProperty(step)) {
+            this._userPlugins[step] = [plugin];
+        } else {
+            this._userPlugins[step].push(plugin);
+        }
+    }
+
+    /**
+     * Sends an email using the preselected transport object
+     *
+     * @param {Object} data E-data description
+     * @param {Function} callback Callback to run once the sending succeeded or failed
+     */
+    sendMail(data, callback) {
+        let promise;
+
+        if (!callback && typeof Promise === 'function') {
+            promise = new Promise((resolve, reject) => {
+                callback = shared.callbackPromise(resolve, reject);
+            });
+        }
+
+        if (typeof this.getSocket === 'function') {
+            this.transporter.getSocket = this.getSocket;
+            this.getSocket = false;
+        }
+
+        let mail = new MailMessage(this, data);
+
+        this.logger.debug({
+            tnx: 'transport',
+            name: this.transporter.name,
+            version: this.transporter.version,
+            action: 'send'
+        }, 'Sending mail using %s/%s', this.transporter.name, this.transporter.version);
+
+        this._processPlugins('compile', mail, err => {
+            if (err) {
+                this.logger.error({
+                    err,
+                    tnx: 'plugin',
+                    action: 'compile'
+                }, 'PluginCompile Error: %s', err.message);
+                return callback(err);
+            }
+
+            mail.message = new MailComposer(mail.data).compile();
+
+            mail.setMailerHeader();
+            mail.setPriorityHeaders();
+            mail.setListHeaders();
+
+            this._processPlugins('stream', mail, err => {
+                if (err) {
+                    this.logger.error({
+                        err,
+                        tnx: 'plugin',
+                        action: 'stream'
+                    }, 'PluginStream Error: %s', err.message);
+                    return callback(err);
+                }
+
+                if (mail.data.dkim || this.dkim) {
+                    mail.message.processFunc(input => {
+                        let dkim = mail.data.dkim ? new DKIM(mail.data.dkim) : this.dkim;
+                        this.logger.debug({
+                            tnx: 'DKIM',
+                            messageId: mail.message.messageId(),
+                            dkimDomains: dkim.keys.map(key => key.keySelector + '.' + key.domainName).join(', ')
+                        }, 'Signing outgoing message with %s keys', dkim.keys.length);
+                        return dkim.sign(input, mail.data._dkim);
+                    });
+                }
+
+                this.transporter.send(mail, (...args) => {
+                    if (args[0]) {
+                        this.logger.error({
+                            err: args[0],
+                            tnx: 'transport',
+                            action: 'send'
+                        }, 'Send Error: %s', args[0].message);
+                    }
+                    callback(...args);
+                });
+            });
+        });
+
+        return promise;
+    }
+
+    getVersionString() {
+        return util.format(
+            '%s (%s; +%s; %s/%s)',
+            packageData.name,
+            packageData.version,
+            packageData.homepage,
+            this.transporter.name,
+            this.transporter.version
+        );
+    }
+
+    _processPlugins(step, mail, callback) {
+        step = (step || '').toString();
+
+        if (!this._userPlugins.hasOwnProperty(step)) {
+            return callback();
+        }
+
+        let userPlugins = this._userPlugins[step] || [];
+        let defaultPlugins = this._defaultPlugins[step] || [];
+
+        if (userPlugins.length) {
+            this.logger.debug({
+                tnx: 'transaction',
+                pluginCount: userPlugins.length,
+                step
+            }, 'Using %s plugins for %s', userPlugins.length, step);
+        }
+
+        if (userPlugins.length + defaultPlugins.length === 0) {
+            return callback();
+        }
+
+        let pos = 0;
+        let block = 'default';
+        let processPlugins = () => {
+            let curplugins = block === 'default' ? defaultPlugins : userPlugins;
+            if (pos >= curplugins.length) {
+                if (block === 'default' && userPlugins.length) {
+                    block = 'user';
+                    pos = 0;
+                    curplugins = userPlugins;
+                } else {
+                    return callback();
+                }
+            }
+            let plugin = curplugins[pos++];
+            plugin(mail, err => {
+                if (err) {
+                    return callback(err);
+                }
+                processPlugins();
+            });
+        };
+
+        processPlugins();
+    }
+
+    /**
+     * Sets up proxy handler for a Nodemailer object
+     *
+     * @param {String} proxyUrl Proxy configuration url
+     */
+    setupProxy(proxyUrl) {
+        let proxy = urllib.parse(proxyUrl);
+
+        // setup socket handler for the mailer object
+        this.getSocket = (options, callback) => {
+            let protocol = proxy.protocol.replace(/:$/, '').toLowerCase();
+
+            if (this.meta.has('proxy_handler_' + protocol)) {
+                return this.meta.get('proxy_handler_' + protocol)(proxy, options, callback);
+            }
+
+            switch (protocol) {
+                // Connect using a HTTP CONNECT method
+                case 'http':
+                case 'https':
+                    httpProxyClient(proxy.href, options.port, options.host, (err, socket) => {
+                        if (err) {
+                            return callback(err);
+                        }
+                        return callback(null, {
+                            connection: socket
+                        });
+                    });
+                    return;
+                case 'socks':
+                case 'socks5':
+                case 'socks4':
+                case 'socks4a':
+                    {
+                        if (!this.meta.has('proxy_socks_module')) {
+                            return callback(new Error('Socks module not loaded'));
+                        }
+
+                        let connect = ipaddress => {
+                            this.meta.get('proxy_socks_module').createConnection({
+                                proxy: {
+                                    ipaddress,
+                                    port: proxy.port,
+                                    type: Number(proxy.protocol.replace(/\D/g, '')) || 5
+                                },
+                                target: {
+                                    host: options.host,
+                                    port: options.port
+                                },
+                                command: 'connect',
+                                authentication: !proxy.auth ? false : {
+                                    username: decodeURIComponent(proxy.auth.split(':').shift()),
+                                    password: decodeURIComponent(proxy.auth.split(':').pop())
+                                }
+                            }, (err, socket) => {
+                                if (err) {
+                                    return callback(err);
+                                }
+                                return callback(null, {
+                                    connection: socket
+                                });
+                            });
+                        };
+
+                        if (net.isIP(proxy.hostname)) {
+                            return connect(proxy.hostname);
+                        }
+
+                        return dns.resolve(proxy.hostname, (err, address) => {
+                            if (err) {
+                                return callback(err);
+                            }
+                            connect(address);
+                        });
+                    }
+            }
+            callback(new Error('Unknown proxy configuration'));
+        };
+    }
+
+    _convertDataImages(mail, callback) {
+        if (!this.options.attachDataUrls && !mail.data.attachDataUrls || !mail.data.html) {
+            return callback();
+        }
+        mail.resolveContent(mail.data, 'html', (err, html) => {
+            if (err) {
+                return callback(err);
+            }
+            let cidCounter = 0;
+            html = (html || '').toString().replace(/(<img\b[^>]* src\s*=[\s"']*)(data:([^;]+);[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => {
+                let cid = crypto.randomBytes(10).toString('hex') + '@localhost';
+                if (!mail.data.attachments) {
+                    mail.data.attachments = [];
+                }
+                if (!Array.isArray(mail.data.attachments)) {
+                    mail.data.attachments = [].concat(mail.data.attachments || []);
+                }
+                mail.data.attachments.push({
+                    path: dataUri,
+                    cid,
+                    filename: 'image-' + (++cidCounter) + '.' + mimeTypes.detectExtension(mimeType)
+                });
+                return prefix + 'cid:' + cid;
+            });
+            mail.data.html = html;
+            callback();
+        });
+    }
+
+    set(key, value) {
+        return this.meta.set(key, value);
+    }
+
+    get(key) {
+        return this.meta.get(key);
+    }
+}
+
+module.exports = Mail;
diff --git a/node_modules/nodemailer/lib/mailer/mail-message.js b/node_modules/nodemailer/lib/mailer/mail-message.js
new file mode 100644
index 0000000..5a500ee
--- /dev/null
+++ b/node_modules/nodemailer/lib/mailer/mail-message.js
@@ -0,0 +1,206 @@
+'use strict';
+
+const shared = require('../shared');
+const MimeNode = require('../mime-node');
+
+class MailMessage {
+    constructor(mailer, data) {
+        this.mailer = mailer;
+        this.data = {};
+        this.message = null;
+
+        data = data || {};
+        let options = mailer.options || {};
+        let defaults = mailer._defaults || {};
+
+        Object.keys(data).forEach(key => {
+            this.data[key] = data[key];
+        });
+
+        this.data.headers = this.data.headers || {};
+
+        // apply defaults
+        Object.keys(defaults).forEach(key => {
+            if (!(key in this.data)) {
+                this.data[key] = defaults[key];
+            } else if (key === 'headers') {
+                // headers is a special case. Allow setting individual default headers
+                Object.keys(defaults.headers).forEach(key => {
+                    if (!(key in this.data.headers)) {
+                        this.data.headers[key] = defaults.headers[key];
+                    }
+                });
+            }
+        });
+
+        // force specific keys from transporter options
+        ['disableFileAccess', 'disableUrlAccess'].forEach(key => {
+            if (key in options) {
+                this.data[key] = options[key];
+            }
+        });
+    }
+
+    resolveContent(...args) {
+        return shared.resolveContent(...args);
+    }
+
+    resolveAll(callback) {
+        let keys = [
+            [this.data, 'html'],
+            [this.data, 'text'],
+            [this.data, 'watchHtml'],
+            [this.data, 'icalEvent']
+        ];
+
+        if (this.data.alternatives && this.data.alternatives.length) {
+            this.data.alternatives.forEach((alternative, i) => {
+                keys.push([this.data.alternatives, i]);
+            });
+        }
+
+        if (this.data.attachments && this.data.attachments.length) {
+            this.data.attachments.forEach((alternative, i) => {
+                keys.push([this.data.attachments, i]);
+            });
+        }
+
+        let mimeNode = new MimeNode();
+
+        let addressKeys = ['from', 'to', 'cc', 'bcc', 'sender', 'replyTo'];
+
+        addressKeys.forEach(address => {
+            let value;
+            if (this.message) {
+                value = [].concat(mimeNode._parseAddresses(this.message.getHeader(address === 'replyTo' ? 'reply-to' : address)) || []);
+            } else if (this.data[address]) {
+                value = [].concat(mimeNode._parseAddresses(this.data[address]) || []);
+            }
+            if (value && value.length) {
+                this.data[address] = value;
+            } else if (address in this.data) {
+                this.data[address] = null;
+            }
+
+        });
+
+        let singleKeys = ['from', 'sender', 'replyTo'];
+        singleKeys.forEach(address => {
+            if (this.data[address]) {
+                this.data[address] = this.data[address].shift();
+            }
+        });
+
+        let pos = 0;
+        let resolveNext = () => {
+            if (pos >= keys.length) {
+                return callback(null, this.data);
+            }
+            let args = keys[pos++];
+            if (!args[0] || !args[0][args[1]]) {
+                return resolveNext();
+            }
+            shared.resolveContent(...args, (err, value) => {
+                if (err) {
+                    return callback(err);
+                }
+
+                let node = {
+                    content: value
+                };
+                if (args[0][args[1]] && typeof args[0][args[1]] === 'object' && !Buffer.isBuffer(args[0][args[1]])) {
+                    Object.keys(args[0][args[1]]).forEach(key => {
+                        if (!(key in node) && !['content', 'path', 'href', 'raw'].includes(key)) {
+                            node[key] = args[0][args[1]][key];
+                        }
+                    });
+                }
+
+                args[0][args[1]] = node;
+                resolveNext();
+            });
+        };
+
+        setImmediate(() => resolveNext());
+    }
+
+    setMailerHeader() {
+        if (!this.message || !this.data.xMailer) {
+            return;
+        }
+        this.message.setHeader('X-Mailer', this.data.xMailer);
+    }
+
+    setPriorityHeaders() {
+        if (!this.message || !this.data.priority) {
+            return;
+        }
+        switch ((this.data.priority || '').toString().toLowerCase()) {
+            case 'high':
+                this.message.setHeader('X-Priority', '1 (Highest)');
+                this.message.setHeader('X-MSMail-Priority', 'High');
+                this.message.setHeader('Importance', 'High');
+                break;
+            case 'low':
+                this.message.setHeader('X-Priority', '5 (Lowest)');
+                this.message.setHeader('X-MSMail-Priority', 'Low');
+                this.message.setHeader('Importance', 'Low');
+                break;
+            default:
+                // do not add anything, since all messages are 'Normal' by default
+        }
+    }
+
+    setListHeaders() {
+        if (!this.message || !this.data.list || typeof this.data.list !== 'object') {
+            return;
+        }
+        // add optional List-* headers
+        if (this.data.list && typeof this.data.list === 'object') {
+            this._getListHeaders(this.data.list).forEach(listHeader => {
+                listHeader.value.forEach(value => {
+                    this.message.addHeader(listHeader.key, value);
+                });
+            });
+        }
+    }
+
+    _getListHeaders(listData) {
+        // make sure an url looks like <protocol:url>
+        return Object.keys(listData).map(key => ({
+            key: 'list-' + key.toLowerCase().trim(),
+            value: [].concat(listData[key] || []).map(value => {
+                if (typeof value === 'string') {
+                    return this._formatListUrl(value);
+                }
+                return {
+                    prepared: true,
+                    value: [].concat(value || []).map(value => {
+                        if (typeof value === 'string') {
+                            return this._formatListUrl(value);
+                        }
+                        if (value && value.url) {
+                            return this._formatListUrl(value.url) + (value.comment ? ' (' + value.comment + ')' : '');
+                        }
+                        return '';
+                    }).join(', ')
+                };
+            })
+        }));
+    }
+
+    _formatListUrl(url) {
+        url = url.replace(/[\s<]+|[\s>]+/g, '');
+        if (/^(https?|mailto|ftp):/.test(url)) {
+            return '<' + url + '>';
+        }
+        if (/^[^@]+@[^@]+$/.test(url)) {
+            return '<mailto:' + url + '>';
+        }
+
+        return '<http://' + url + '>';
+    }
+
+}
+
+module.exports = MailMessage;
diff --git a/node_modules/nodemailer/lib/mime-funcs/index.js b/node_modules/nodemailer/lib/mime-funcs/index.js
new file mode 100644
index 0000000..243d585
--- /dev/null
+++ b/node_modules/nodemailer/lib/mime-funcs/index.js
@@ -0,0 +1,608 @@
+/* eslint no-control-regex:0 */
+
+'use strict';
+
+const base64 = require('../base64');
+const qp = require('../qp');
+const mimeTypes = require('./mime-types');
+
+module.exports = {
+
+    /**
+     * Checks if a value is plaintext string (uses only printable 7bit chars)
+     *
+     * @param {String} value String to be tested
+     * @returns {Boolean} true if it is a plaintext string
+     */
+    isPlainText(value) {
+        if (typeof value !== 'string' || /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/.test(value)) {
+            return false;
+        } else {
+            return true;
+        }
+    },
+
+    /**
+     * Checks if a multi line string containes lines longer than the selected value.
+     *
+     * Useful when detecting if a mail message needs any processing at all –
+     * if only plaintext characters are used and lines are short, then there is
+     * no need to encode the values in any way. If the value is plaintext but has
+     * longer lines then allowed, then use format=flowed
+     *
+     * @param {Number} lineLength Max line length to check for
+     * @returns {Boolean} Returns true if there is at least one line longer than lineLength chars
+     */
+    hasLongerLines(str, lineLength) {
+        if (str.length > 128 * 1024) {
+            // do not test strings longer than 128kB
+            return true;
+        }
+        return new RegExp('^.{' + (lineLength + 1) + ',}', 'm').test(str);
+    },
+
+    /**
+     * Encodes a string or an Buffer to an UTF-8 MIME Word (rfc2047)
+     *
+     * @param {String|Buffer} data String to be encoded
+     * @param {String} mimeWordEncoding='Q' Encoding for the mime word, either Q or B
+     * @param {Number} [maxLength=0] If set, split mime words into several chunks if needed
+     * @return {String} Single or several mime words joined together
+     */
+    encodeWord(data, mimeWordEncoding, maxLength) {
+        mimeWordEncoding = (mimeWordEncoding || 'Q').toString().toUpperCase().trim().charAt(0);
+        maxLength = maxLength || 0;
+
+        let encodedStr;
+        let toCharset = 'UTF-8';
+
+        if (maxLength && maxLength > 7 + toCharset.length) {
+            maxLength -= (7 + toCharset.length);
+        }
+
+        if (mimeWordEncoding === 'Q') {
+            // https://tools.ietf.org/html/rfc2047#section-5 rule (3)
+            encodedStr = qp.encode(data).replace(/[^a-z0-9!*+\-\/=]/ig, chr => {
+                let ord = chr.charCodeAt(0).toString(16).toUpperCase();
+                if (chr === ' ') {
+                    return '_';
+                } else {
+                    return '=' + (ord.length === 1 ? '0' + ord : ord);
+                }
+            });
+        } else if (mimeWordEncoding === 'B') {
+            encodedStr = typeof data === 'string' ? data : base64.encode(data);
+            maxLength = maxLength ? Math.max(3, (maxLength - maxLength % 4) / 4 * 3) : 0;
+        }
+
+        if (maxLength && (mimeWordEncoding !== 'B' ? encodedStr : base64.encode(data)).length > maxLength) {
+            if (mimeWordEncoding === 'Q') {
+                encodedStr = this.splitMimeEncodedString(encodedStr, maxLength).join('?= =?' + toCharset + '?' + mimeWordEncoding + '?');
+            } else {
+                // RFC2047 6.3 (2) states that encoded-word must include an integral number of characters, so no chopping unicode sequences
+                let parts = [];
+                let lpart = '';
+                for (let i = 0, len = encodedStr.length; i < len; i++) {
+                    let chr = encodedStr.charAt(i);
+                    // check if we can add this character to the existing string
+                    // without breaking byte length limit
+                    if (Buffer.byteLength(lpart + chr) <= maxLength || i === 0) {
+                        lpart += chr;
+                    } else {
+                        // we hit the length limit, so push the existing string and start over
+                        parts.push(base64.encode(lpart));
+                        lpart = chr;
+                    }
+                }
+                if (lpart) {
+                    parts.push(base64.encode(lpart));
+                }
+
+                if (parts.length > 1) {
+                    encodedStr = parts.join('?= =?' + toCharset + '?' + mimeWordEncoding + '?');
+                } else {
+                    encodedStr = parts.join('');
+                }
+            }
+        } else if (mimeWordEncoding === 'B') {
+            encodedStr = base64.encode(data);
+        }
+
+        return '=?' + toCharset + '?' + mimeWordEncoding + '?' + encodedStr + (encodedStr.substr(-2) === '?=' ? '' : '?=');
+    },
+
+    /**
+     * Finds word sequences with non ascii text and converts these to mime words
+     *
+     * @param {String} value String to be encoded
+     * @param {String} mimeWordEncoding='Q' Encoding for the mime word, either Q or B
+     * @param {Number} [maxLength=0] If set, split mime words into several chunks if needed
+     * @return {String} String with possible mime words
+     */
+    encodeWords(value, mimeWordEncoding, maxLength) {
+        maxLength = maxLength || 0;
+
+        let encodedValue;
+
+        // find first word with a non-printable ascii in it
+        let firstMatch = value.match(/(?:^|\s)([^\s]*[\u0080-\uFFFF])/);
+        if (!firstMatch) {
+            return value;
+        }
+
+        // find the last word with a non-printable ascii in it
+        let lastMatch = value.match(/([\u0080-\uFFFF][^\s]*)[^\u0080-\uFFFF]*$/);
+        if (!lastMatch) {
+            // should not happen
+            return value;
+        }
+
+        let startIndex = firstMatch.index + (firstMatch[0].match(/[^\s]/) || {
+            index: 0
+        }).index;
+        let endIndex = lastMatch.index + (lastMatch[1] || '').length;
+
+        encodedValue =
+            (startIndex ? value.substr(0, startIndex) : '') + this.encodeWord(value.substring(startIndex, endIndex), mimeWordEncoding || 'Q', maxLength) +
+            (endIndex < value.length ? value.substr(endIndex) : '');
+
+        return encodedValue;
+    },
+
+    /**
+     * Joins parsed header value together as 'value; param1=value1; param2=value2'
+     * PS: We are following RFC 822 for the list of special characters that we need to keep in quotes.
+     *      Refer: https://www.w3.org/Protocols/rfc1341/4_Content-Type.html
+     * @param {Object} structured Parsed header value
+     * @return {String} joined header value
+     */
+    buildHeaderValue(structured) {
+        let paramsArray = [];
+
+        Object.keys(structured.params || {}).forEach(param => {
+            // filename might include unicode characters so it is a special case
+            // other values probably do not
+            let value = structured.params[param];
+            if (!this.isPlainText(value) || value.length >= 75) {
+                this.buildHeaderParam(param, value, 50).forEach(encodedParam => {
+                    if (!/[\s"\\;:\/=\(\),<>@\[\]\?]|^[\-']|'$/.test(encodedParam.value) || encodedParam.key.substr(-1) === '*') {
+                        paramsArray.push(encodedParam.key + '=' + encodedParam.value);
+                    } else {
+                        paramsArray.push(encodedParam.key + '=' + JSON.stringify(encodedParam.value));
+                    }
+                });
+            } else if (/[\s'"\\;:\/=\(\),<>@\[\]\?]|^\-/.test(value)) {
+                paramsArray.push(param + '=' + JSON.stringify(value));
+            } else {
+                paramsArray.push(param + '=' + value);
+            }
+        });
+
+        return structured.value + (paramsArray.length ? '; ' + paramsArray.join('; ') : '');
+    },
+
+    /**
+     * Encodes a string or an Buffer to an UTF-8 Parameter Value Continuation encoding (rfc2231)
+     * Useful for splitting long parameter values.
+     *
+     * For example
+     *      title="unicode string"
+     * becomes
+     *     title*0*=utf-8''unicode
+     *     title*1*=%20string
+     *
+     * @param {String|Buffer} data String to be encoded
+     * @param {Number} [maxLength=50] Max length for generated chunks
+     * @param {String} [fromCharset='UTF-8'] Source sharacter set
+     * @return {Array} A list of encoded keys and headers
+     */
+    buildHeaderParam(key, data, maxLength) {
+        let list = [];
+        let encodedStr = typeof data === 'string' ? data : (data || '').toString();
+        let encodedStrArr;
+        let chr, ord;
+        let line;
+        let startPos = 0;
+        let i, len;
+
+        maxLength = maxLength || 50;
+
+        // process ascii only text
+        if (this.isPlainText(data)) {
+
+            // check if conversion is even needed
+            if (encodedStr.length <= maxLength) {
+                return [{
+                    key,
+                    value: encodedStr
+                }];
+            }
+
+            encodedStr = encodedStr.replace(new RegExp('.{' + maxLength + '}', 'g'), str => {
+                list.push({
+                    line: str
+                });
+                return '';
+            });
+
+            if (encodedStr) {
+                list.push({
+                    line: encodedStr
+                });
+            }
+
+        } else {
+
+            if (/[\uD800-\uDBFF]/.test(encodedStr)) {
+                // string containts surrogate pairs, so normalize it to an array of bytes
+                encodedStrArr = [];
+                for (i = 0, len = encodedStr.length; i < len; i++) {
+                    chr = encodedStr.charAt(i);
+                    ord = chr.charCodeAt(0);
+                    if (ord >= 0xD800 && ord <= 0xDBFF && i < len - 1) {
+                        chr += encodedStr.charAt(i + 1);
+                        encodedStrArr.push(chr);
+                        i++;
+                    } else {
+                        encodedStrArr.push(chr);
+                    }
+                }
+                encodedStr = encodedStrArr;
+            }
+
+            // first line includes the charset and language info and needs to be encoded
+            // even if it does not contain any unicode characters
+            line = 'utf-8\'\'';
+            let encoded = true;
+            startPos = 0;
+
+            // process text with unicode or special chars
+            for (i = 0, len = encodedStr.length; i < len; i++) {
+
+                chr = encodedStr[i];
+
+                if (encoded) {
+                    chr = this.safeEncodeURIComponent(chr);
+                } else {
+                    // try to urlencode current char
+                    chr = chr === ' ' ? chr : this.safeEncodeURIComponent(chr);
+                    // By default it is not required to encode a line, the need
+                    // only appears when the string contains unicode or special chars
+                    // in this case we start processing the line over and encode all chars
+                    if (chr !== encodedStr[i]) {
+                        // Check if it is even possible to add the encoded char to the line
+                        // If not, there is no reason to use this line, just push it to the list
+                        // and start a new line with the char that needs encoding
+                        if ((this.safeEncodeURIComponent(line) + chr).length >= maxLength) {
+                            list.push({
+                                line,
+                                encoded
+                            });
+                            line = '';
+                            startPos = i - 1;
+                        } else {
+                            encoded = true;
+                            i = startPos;
+                            line = '';
+                            continue;
+                        }
+                    }
+                }
+
+                // if the line is already too long, push it to the list and start a new one
+                if ((line + chr).length >= maxLength) {
+                    list.push({
+                        line,
+                        encoded
+                    });
+                    line = chr = encodedStr[i] === ' ' ? ' ' : this.safeEncodeURIComponent(encodedStr[i]);
+                    if (chr === encodedStr[i]) {
+                        encoded = false;
+                        startPos = i - 1;
+                    } else {
+                        encoded = true;
+                    }
+                } else {
+                    line += chr;
+                }
+            }
+
+            if (line) {
+                list.push({
+                    line,
+                    encoded
+                });
+            }
+        }
+
+        return list.map((item, i) => ({
+            // encoded lines: {name}*{part}*
+            // unencoded lines: {name}*{part}
+            // if any line needs to be encoded then the first line (part==0) is always encoded
+            key: key + '*' + i + (item.encoded ? '*' : ''),
+            value: item.line
+        }));
+    },
+
+    /**
+     * Parses a header value with key=value arguments into a structured
+     * object.
+     *
+     *   parseHeaderValue('content-type: text/plain; CHARSET='UTF-8'') ->
+     *   {
+     *     'value': 'text/plain',
+     *     'params': {
+     *       'charset': 'UTF-8'
+     *     }
+     *   }
+     *
+     * @param {String} str Header value
+     * @return {Object} Header value as a parsed structure
+     */
+    parseHeaderValue(str) {
+        let response = {
+            value: false,
+            params: {}
+        };
+        let key = false;
+        let value = '';
+        let type = 'value';
+        let quote = false;
+        let escaped = false;
+        let chr;
+
+        for (let i = 0, len = str.length; i < len; i++) {
+            chr = str.charAt(i);
+            if (type === 'key') {
+                if (chr === '=') {
+                    key = value.trim().toLowerCase();
+                    type = 'value';
+                    value = '';
+                    continue;
+                }
+                value += chr;
+            } else {
+                if (escaped) {
+                    value += chr;
+                } else if (chr === '\\') {
+                    escaped = true;
+                    continue;
+                } else if (quote && chr === quote) {
+                    quote = false;
+                } else if (!quote && chr === '"') {
+                    quote = chr;
+                } else if (!quote && chr === ';') {
+                    if (key === false) {
+                        response.value = value.trim();
+                    } else {
+                        response.params[key] = value.trim();
+                    }
+                    type = 'key';
+                    value = '';
+                } else {
+                    value += chr;
+                }
+                escaped = false;
+
+            }
+        }
+
+        if (type === 'value') {
+            if (key === false) {
+                response.value = value.trim();
+            } else {
+                response.params[key] = value.trim();
+            }
+        } else if (value.trim()) {
+            response.params[value.trim().toLowerCase()] = '';
+        }
+
+        // handle parameter value continuations
+        // https://tools.ietf.org/html/rfc2231#section-3
+
+        // preprocess values
+        Object.keys(response.params).forEach(key => {
+            let actualKey, nr, match, value;
+            if ((match = key.match(/(\*(\d+)|\*(\d+)\*|\*)$/))) {
+                actualKey = key.substr(0, match.index);
+                nr = Number(match[2] || match[3]) || 0;
+
+                if (!response.params[actualKey] || typeof response.params[actualKey] !== 'object') {
+                    response.params[actualKey] = {
+                        charset: false,
+                        values: []
+                    };
+                }
+
+                value = response.params[key];
+
+                if (nr === 0 && match[0].substr(-1) === '*' && (match = value.match(/^([^']*)'[^']*'(.*)$/))) {
+                    response.params[actualKey].charset = match[1] || 'iso-8859-1';
+                    value = match[2];
+                }
+
+                response.params[actualKey].values[nr] = value;
+
+                // remove the old reference
+                delete response.params[key];
+            }
+        });
+
+        // concatenate split rfc2231 strings and convert encoded strings to mime encoded words
+        Object.keys(response.params).forEach(key => {
+            let value;
+            if (response.params[key] && Array.isArray(response.params[key].values)) {
+                value = response.params[key].values.map(val => val || '').join('');
+
+                if (response.params[key].charset) {
+                    // convert "%AB" to "=?charset?Q?=AB?="
+                    response.params[key] = '=?' +
+                        response.params[key].charset +
+                        '?Q?' +
+                        value.
+                    // fix invalidly encoded chars
+                    replace(/[=\?_\s]/g,
+                        s => {
+                            let c = s.charCodeAt(0).toString(16);
+                            if (s === ' ') {
+                                return '_';
+                            } else {
+                                return '%' + (c.length < 2 ? '0' : '') + c;
+                            }
+                        }
+                    ).
+                    // change from urlencoding to percent encoding
+                    replace(/%/g, '=') +
+                        '?=';
+                } else {
+                    response.params[key] = value;
+                }
+            }
+        });
+
+        return response;
+    },
+
+    /**
+     * Returns file extension for a content type string. If no suitable extensions
+     * are found, 'bin' is used as the default extension
+     *
+     * @param {String} mimeType Content type to be checked for
+     * @return {String} File extension
+     */
+    detectExtension: mimeType => mimeTypes.detectExtension(mimeType),
+
+    /**
+     * Returns content type for a file extension. If no suitable content types
+     * are found, 'application/octet-stream' is used as the default content type
+     *
+     * @param {String} extension Extension to be checked for
+     * @return {String} File extension
+     */
+    detectMimeType: extension => mimeTypes.detectMimeType(extension),
+
+    /**
+     * Folds long lines, useful for folding header lines (afterSpace=false) and
+     * flowed text (afterSpace=true)
+     *
+     * @param {String} str String to be folded
+     * @param {Number} [lineLength=76] Maximum length of a line
+     * @param {Boolean} afterSpace If true, leave a space in th end of a line
+     * @return {String} String with folded lines
+     */
+    foldLines(str, lineLength, afterSpace) {
+        str = (str || '').toString();
+        lineLength = lineLength || 76;
+
+        let pos = 0,
+            len = str.length,
+            result = '',
+            line, match;
+
+        while (pos < len) {
+            line = str.substr(pos, lineLength);
+            if (line.length < lineLength) {
+                result += line;
+                break;
+            }
+            if ((match = line.match(/^[^\n\r]*(\r?\n|\r)/))) {
+                line = match[0];
+                result += line;
+                pos += line.length;
+                continue;
+            } else if ((match = line.match(/(\s+)[^\s]*$/)) && match[0].length - (afterSpace ? (match[1] || '').length : 0) < line.length) {
+                line = line.substr(0, line.length - (match[0].length - (afterSpace ? (match[1] || '').length : 0)));
+            } else if ((match = str.substr(pos + line.length).match(/^[^\s]+(\s*)/))) {
+                line = line + match[0].substr(0, match[0].length - (!afterSpace ? (match[1] || '').length : 0));
+            }
+
+            result += line;
+            pos += line.length;
+            if (pos < len) {
+                result += '\r\n';
+            }
+        }
+
+        return result;
+    },
+
+    /**
+     * Splits a mime encoded string. Needed for dividing mime words into smaller chunks
+     *
+     * @param {String} str Mime encoded string to be split up
+     * @param {Number} maxlen Maximum length of characters for one part (minimum 12)
+     * @return {Array} Split string
+     */
+    splitMimeEncodedString: (str, maxlen) => {
+        let curLine, match, chr, done,
+            lines = [];
+
+        // require at least 12 symbols to fit possible 4 octet UTF-8 sequences
+        maxlen = Math.max(maxlen || 0, 12);
+
+        while (str.length) {
+            curLine = str.substr(0, maxlen);
+
+            // move incomplete escaped char back to main
+            if ((match = curLine.match(/\=[0-9A-F]?$/i))) {
+                curLine = curLine.substr(0, match.index);
+            }
+
+            done = false;
+            while (!done) {
+                done = true;
+                // check if not middle of a unicode char sequence
+                if ((match = str.substr(curLine.length).match(/^\=([0-9A-F]{2})/i))) {
+                    chr = parseInt(match[1], 16);
+                    // invalid sequence, move one char back anc recheck
+                    if (chr < 0xC2 && chr > 0x7F) {
+                        curLine = curLine.substr(0, curLine.length - 3);
+                        done = false;
+                    }
+                }
+            }
+
+            if (curLine.length) {
+                lines.push(curLine);
+            }
+            str = str.substr(curLine.length);
+        }
+
+        return lines;
+    },
+
+    encodeURICharComponent: chr => {
+        let res = '';
+        let ord = chr.charCodeAt(0).toString(16).toUpperCase();
+
+        if (ord.length % 2) {
+            ord = '0' + ord;
+        }
+
+        if (ord.length > 2) {
+            for (let i = 0, len = ord.length / 2; i < len; i++) {
+                res += '%' + ord.substr(i, 2);
+            }
+        } else {
+            res += '%' + ord;
+        }
+
+        return res;
+    },
+
+    safeEncodeURIComponent(str) {
+        str = (str || '').toString();
+
+        try {
+            // might throw if we try to encode invalid sequences, eg. partial emoji
+            str = encodeURIComponent(str);
+        } catch (E) {
+            // should never run
+            return str.replace(/[^\x00-\x1F *'()<>@,;:\\"\[\]?=\u007F-\uFFFF]+/g, '');
+        }
+
+        // ensure chars that are not handled by encodeURICompent are converted as well
+        return str.replace(/[\x00-\x1F *'()<>@,;:\\"\[\]?=\u007F-\uFFFF]/g, chr => this.encodeURICharComponent(chr));
+    }
+
+};
diff --git a/node_modules/nodemailer/lib/mime-funcs/mime-types.js b/node_modules/nodemailer/lib/mime-funcs/mime-types.js
new file mode 100644
index 0000000..1880432
--- /dev/null
+++ b/node_modules/nodemailer/lib/mime-funcs/mime-types.js
@@ -0,0 +1,2359 @@
+/* eslint quote-props: 0 */
+
+'use strict';
+
+const path = require('path');
+
+const defaultMimeType = 'application/octet-stream';
+const defaultExtension = 'bin';
+
+const mimeTypes = new Map([
+    ['application/acad', 'dwg'],
+    ['application/applixware', 'aw'],
+    ['application/arj', 'arj'],
+    ['application/atom+xml', 'xml'],
+    ['application/atomcat+xml', 'atomcat'],
+    ['application/atomsvc+xml', 'atomsvc'],
+    ['application/base64', ['mm', 'mme']],
+    ['application/binhex', 'hqx'],
+    ['application/binhex4', 'hqx'],
+    ['application/book', ['book', 'boo']],
+    ['application/ccxml+xml,', 'ccxml'],
+    ['application/cdf', 'cdf'],
+    ['application/cdmi-capability', 'cdmia'],
+    ['application/cdmi-container', 'cdmic'],
+    ['application/cdmi-domain', 'cdmid'],
+    ['application/cdmi-object', 'cdmio'],
+    ['application/cdmi-queue', 'cdmiq'],
+    ['application/clariscad', 'ccad'],
+    ['application/commonground', 'dp'],
+    ['application/cu-seeme', 'cu'],
+    ['application/davmount+xml', 'davmount'],
+    ['application/drafting', 'drw'],
+    ['application/dsptype', 'tsp'],
+    ['application/dssc+der', 'dssc'],
+    ['application/dssc+xml', 'xdssc'],
+    ['application/dxf', 'dxf'],
+    ['application/ecmascript', ['js', 'es']],
+    ['application/emma+xml', 'emma'],
+    ['application/envoy', 'evy'],
+    ['application/epub+zip', 'epub'],
+    ['application/excel', ['xls',
+        'xl',
+        'xla',
+        'xlb',
+        'xlc',
+        'xld',
+        'xlk',
+        'xll',
+        'xlm',
+        'xlt',
+        'xlv',
+        'xlw'
+    ]],
+    ['application/exi', 'exi'],
+    ['application/font-tdpfr', 'pfr'],
+    ['application/fractals', 'fif'],
+    ['application/freeloader', 'frl'],
+    ['application/futuresplash', 'spl'],
+    ['application/gnutar', 'tgz'],
+    ['application/groupwise', 'vew'],
+    ['application/hlp', 'hlp'],
+    ['application/hta', 'hta'],
+    ['application/hyperstudio', 'stk'],
+    ['application/i-deas', 'unv'],
+    ['application/iges', ['iges', 'igs']],
+    ['application/inf', 'inf'],
+    ['application/internet-property-stream', 'acx'],
+    ['application/ipfix', 'ipfix'],
+    ['application/java', 'class'],
+    ['application/java-archive', 'jar'],
+    ['application/java-byte-code', 'class'],
+    ['application/java-serialized-object', 'ser'],
+    ['application/java-vm', 'class'],
+    ['application/javascript', 'js'],
+    ['application/json', 'json'],
+    ['application/lha', 'lha'],
+    ['application/lzx', 'lzx'],
+    ['application/mac-binary', 'bin'],
+    ['application/mac-binhex', 'hqx'],
+    ['application/mac-binhex40', 'hqx'],
+    ['application/mac-compactpro', 'cpt'],
+    ['application/macbinary', 'bin'],
+    ['application/mads+xml', 'mads'],
+    ['application/marc', 'mrc'],
+    ['application/marcxml+xml', 'mrcx'],
+    ['application/mathematica', 'ma'],
+    ['application/mathml+xml', 'mathml'],
+    ['application/mbedlet', 'mbd'],
+    ['application/mbox', 'mbox'],
+    ['application/mcad', 'mcd'],
+    ['application/mediaservercontrol+xml', 'mscml'],
+    ['application/metalink4+xml', 'meta4'],
+    ['application/mets+xml', 'mets'],
+    ['application/mime', 'aps'],
+    ['application/mods+xml', 'mods'],
+    ['application/mp21', 'm21'],
+    ['application/mp4', 'mp4'],
+    ['application/mspowerpoint', ['ppt', 'pot', 'pps', 'ppz']],
+    ['application/msword', ['doc', 'dot', 'w6w', 'wiz', 'word']],
+    ['application/mswrite', 'wri'],
+    ['application/mxf', 'mxf'],
+    ['application/netmc', 'mcp'],
+    ['application/octet-stream', ['*']],
+    ['application/oda', 'oda'],
+    ['application/oebps-package+xml', 'opf'],
+    ['application/ogg', 'ogx'],
+    ['application/olescript', 'axs'],
+    ['application/onenote', 'onetoc'],
+    ['application/patch-ops-error+xml', 'xer'],
+    ['application/pdf', 'pdf'],
+    ['application/pgp-encrypted', 'asc'],
+    ['application/pgp-signature', 'pgp'],
+    ['application/pics-rules', 'prf'],
+    ['application/pkcs-12', 'p12'],
+    ['application/pkcs-crl', 'crl'],
+    ['application/pkcs10', 'p10'],
+    ['application/pkcs7-mime', ['p7c', 'p7m']],
+    ['application/pkcs7-signature', 'p7s'],
+    ['application/pkcs8', 'p8'],
+    ['application/pkix-attr-cert', 'ac'],
+    ['application/pkix-cert', ['cer', 'crt']],
+    ['application/pkix-crl', 'crl'],
+    ['application/pkix-pkipath', 'pkipath'],
+    ['application/pkixcmp', 'pki'],
+    ['application/plain', 'text'],
+    ['application/pls+xml', 'pls'],
+    ['application/postscript', ['ps', 'ai', 'eps']],
+    ['application/powerpoint', 'ppt'],
+    ['application/pro_eng', ['part', 'prt']],
+    ['application/prs.cww', 'cww'],
+    ['application/pskc+xml', 'pskcxml'],
+    ['application/rdf+xml', 'rdf'],
+    ['application/reginfo+xml', 'rif'],
+    ['application/relax-ng-compact-syntax', 'rnc'],
+    ['application/resource-lists+xml', 'rl'],
+    ['application/resource-lists-diff+xml', 'rld'],
+    ['application/ringing-tones', 'rng'],
+    ['application/rls-services+xml', 'rs'],
+    ['application/rsd+xml', 'rsd'],
+    ['application/rss+xml', 'xml'],
+    ['application/rtf', ['rtf', 'rtx']],
+    ['application/sbml+xml', 'sbml'],
+    ['application/scvp-cv-request', 'scq'],
+    ['application/scvp-cv-response', 'scs'],
+    ['application/scvp-vp-request', 'spq'],
+    ['application/scvp-vp-response', 'spp'],
+    ['application/sdp', 'sdp'],
+    ['application/sea', 'sea'],
+    ['application/set', 'set'],
+    ['application/set-payment-initiation', 'setpay'],
+    ['application/set-registration-initiation', 'setreg'],
+    ['application/shf+xml', 'shf'],
+    ['application/sla', 'stl'],
+    ['application/smil', ['smi', 'smil']],
+    ['application/smil+xml', 'smi'],
+    ['application/solids', 'sol'],
+    ['application/sounder', 'sdr'],
+    ['application/sparql-query', 'rq'],
+    ['application/sparql-results+xml', 'srx'],
+    ['application/srgs', 'gram'],
+    ['application/srgs+xml', 'grxml'],
+    ['application/sru+xml', 'sru'],
+    ['application/ssml+xml', 'ssml'],
+    ['application/step', ['step', 'stp']],
+    ['application/streamingmedia', 'ssm'],
+    ['application/tei+xml', 'tei'],
+    ['application/thraud+xml', 'tfi'],
+    ['application/timestamped-data', 'tsd'],
+    ['application/toolbook', 'tbk'],
+    ['application/vda', 'vda'],
+    ['application/vnd.3gpp.pic-bw-large', 'plb'],
+    ['application/vnd.3gpp.pic-bw-small', 'psb'],
+    ['application/vnd.3gpp.pic-bw-var', 'pvb'],
+    ['application/vnd.3gpp2.tcap', 'tcap'],
+    ['application/vnd.3m.post-it-notes', 'pwn'],
+    ['application/vnd.accpac.simply.aso', 'aso'],
+    ['application/vnd.accpac.simply.imp', 'imp'],
+    ['application/vnd.acucobol', 'acu'],
+    ['application/vnd.acucorp', 'atc'],
+    ['application/vnd.adobe.air-application-installer-package+zip',
+        'air'
+    ],
+    ['application/vnd.adobe.fxp', 'fxp'],
+    ['application/vnd.adobe.xdp+xml', 'xdp'],
+    ['application/vnd.adobe.xfdf', 'xfdf'],
+    ['application/vnd.ahead.space', 'ahead'],
+    ['application/vnd.airzip.filesecure.azf', 'azf'],
+    ['application/vnd.airzip.filesecure.azs', 'azs'],
+    ['application/vnd.amazon.ebook', 'azw'],
+    ['application/vnd.americandynamics.acc', 'acc'],
+    ['application/vnd.amiga.ami', 'ami'],
+    ['application/vnd.android.package-archive', 'apk'],
+    ['application/vnd.anser-web-certificate-issue-initiation',
+        'cii'
+    ],
+    ['application/vnd.anser-web-funds-transfer-initiation', 'fti'],
+    ['application/vnd.antix.game-component', 'atx'],
+    ['application/vnd.apple.installer+xml', 'mpkg'],
+    ['application/vnd.apple.mpegurl', 'm3u8'],
+    ['application/vnd.aristanetworks.swi', 'swi'],
+    ['application/vnd.audiograph', 'aep'],
+    ['application/vnd.blueice.multipass', 'mpm'],
+    ['application/vnd.bmi', 'bmi'],
+    ['application/vnd.businessobjects', 'rep'],
+    ['application/vnd.chemdraw+xml', 'cdxml'],
+    ['application/vnd.chipnuts.karaoke-mmd', 'mmd'],
+    ['application/vnd.cinderella', 'cdy'],
+    ['application/vnd.claymore', 'cla'],
+    ['application/vnd.cloanto.rp9', 'rp9'],
+    ['application/vnd.clonk.c4group', 'c4g'],
+    ['application/vnd.cluetrust.cartomobile-config', 'c11amc'],
+    ['application/vnd.cluetrust.cartomobile-config-pkg', 'c11amz'],
+    ['application/vnd.commonspace', 'csp'],
+    ['application/vnd.contact.cmsg', 'cdbcmsg'],
+    ['application/vnd.cosmocaller', 'cmc'],
+    ['application/vnd.crick.clicker', 'clkx'],
+    ['application/vnd.crick.clicker.keyboard', 'clkk'],
+    ['application/vnd.crick.clicker.palette', 'clkp'],
+    ['application/vnd.crick.clicker.template', 'clkt'],
+    ['application/vnd.crick.clicker.wordbank', 'clkw'],
+    ['application/vnd.criticaltools.wbs+xml', 'wbs'],
+    ['application/vnd.ctc-posml', 'pml'],
+    ['application/vnd.cups-ppd', 'ppd'],
+    ['application/vnd.curl.car', 'car'],
+    ['application/vnd.curl.pcurl', 'pcurl'],
+    ['application/vnd.data-vision.rdz', 'rdz'],
+    ['application/vnd.denovo.fcselayout-link', 'fe_launch'],
+    ['application/vnd.dna', 'dna'],
+    ['application/vnd.dolby.mlp', 'mlp'],
+    ['application/vnd.dpgraph', 'dpg'],
+    ['application/vnd.dreamfactory', 'dfac'],
+    ['application/vnd.dvb.ait', 'ait'],
+    ['application/vnd.dvb.service', 'svc'],
+    ['application/vnd.dynageo', 'geo'],
+    ['application/vnd.ecowin.chart', 'mag'],
+    ['application/vnd.enliven', 'nml'],
+    ['application/vnd.epson.esf', 'esf'],
+    ['application/vnd.epson.msf', 'msf'],
+    ['application/vnd.epson.quickanime', 'qam'],
+    ['application/vnd.epson.salt', 'slt'],
+    ['application/vnd.epson.ssf', 'ssf'],
+    ['application/vnd.eszigno3+xml', 'es3'],
+    ['application/vnd.ezpix-album', 'ez2'],
+    ['application/vnd.ezpix-package', 'ez3'],
+    ['application/vnd.fdf', 'fdf'],
+    ['application/vnd.fdsn.seed', 'seed'],
+    ['application/vnd.flographit', 'gph'],
+    ['application/vnd.fluxtime.clip', 'ftc'],
+    ['application/vnd.framemaker', 'fm'],
+    ['application/vnd.frogans.fnc', 'fnc'],
+    ['application/vnd.frogans.ltf', 'ltf'],
+    ['application/vnd.fsc.weblaunch', 'fsc'],
+    ['application/vnd.fujitsu.oasys', 'oas'],
+    ['application/vnd.fujitsu.oasys2', 'oa2'],
+    ['application/vnd.fujitsu.oasys3', 'oa3'],
+    ['application/vnd.fujitsu.oasysgp', 'fg5'],
+    ['application/vnd.fujitsu.oasysprs', 'bh2'],
+    ['application/vnd.fujixerox.ddd', 'ddd'],
+    ['application/vnd.fujixerox.docuworks', 'xdw'],
+    ['application/vnd.fujixerox.docuworks.binder', 'xbd'],
+    ['application/vnd.fuzzysheet', 'fzs'],
+    ['application/vnd.genomatix.tuxedo', 'txd'],
+    ['application/vnd.geogebra.file', 'ggb'],
+    ['application/vnd.geogebra.tool', 'ggt'],
+    ['application/vnd.geometry-explorer', 'gex'],
+    ['application/vnd.geonext', 'gxt'],
+    ['application/vnd.geoplan', 'g2w'],
+    ['application/vnd.geospace', 'g3w'],
+    ['application/vnd.gmx', 'gmx'],
+    ['application/vnd.google-earth.kml+xml', 'kml'],
+    ['application/vnd.google-earth.kmz', 'kmz'],
+    ['application/vnd.grafeq', 'gqf'],
+    ['application/vnd.groove-account', 'gac'],
+    ['application/vnd.groove-help', 'ghf'],
+    ['application/vnd.groove-identity-message', 'gim'],
+    ['application/vnd.groove-injector', 'grv'],
+    ['application/vnd.groove-tool-message', 'gtm'],
+    ['application/vnd.groove-tool-template', 'tpl'],
+    ['application/vnd.groove-vcard', 'vcg'],
+    ['application/vnd.hal+xml', 'hal'],
+    ['application/vnd.handheld-entertainment+xml', 'zmm'],
+    ['application/vnd.hbci', 'hbci'],
+    ['application/vnd.hhe.lesson-player', 'les'],
+    ['application/vnd.hp-hpgl', ['hgl', 'hpg', 'hpgl']],
+    ['application/vnd.hp-hpid', 'hpid'],
+    ['application/vnd.hp-hps', 'hps'],
+    ['application/vnd.hp-jlyt', 'jlt'],
+    ['application/vnd.hp-pcl', 'pcl'],
+    ['application/vnd.hp-pclxl', 'pclxl'],
+    ['application/vnd.hydrostatix.sof-data', 'sfd-hdstx'],
+    ['application/vnd.hzn-3d-crossword', 'x3d'],
+    ['application/vnd.ibm.minipay', 'mpy'],
+    ['application/vnd.ibm.modcap', 'afp'],
+    ['application/vnd.ibm.rights-management', 'irm'],
+    ['application/vnd.ibm.secure-container', 'sc'],
+    ['application/vnd.iccprofile', 'icc'],
+    ['application/vnd.igloader', 'igl'],
+    ['application/vnd.immervision-ivp', 'ivp'],
+    ['application/vnd.immervision-ivu', 'ivu'],
+    ['application/vnd.insors.igm', 'igm'],
+    ['application/vnd.intercon.formnet', 'xpw'],
+    ['application/vnd.intergeo', 'i2g'],
+    ['application/vnd.intu.qbo', 'qbo'],
+    ['application/vnd.intu.qfx', 'qfx'],
+    ['application/vnd.ipunplugged.rcprofile', 'rcprofile'],
+    ['application/vnd.irepository.package+xml', 'irp'],
+    ['application/vnd.is-xpr', 'xpr'],
+    ['application/vnd.isac.fcs', 'fcs'],
+    ['application/vnd.jam', 'jam'],
+    ['application/vnd.jcp.javame.midlet-rms', 'rms'],
+    ['application/vnd.jisp', 'jisp'],
+    ['application/vnd.joost.joda-archive', 'joda'],
+    ['application/vnd.kahootz', 'ktz'],
+    ['application/vnd.kde.karbon', 'karbon'],
+    ['application/vnd.kde.kchart', 'chrt'],
+    ['application/vnd.kde.kformula', 'kfo'],
+    ['application/vnd.kde.kivio', 'flw'],
+    ['application/vnd.kde.kontour', 'kon'],
+    ['application/vnd.kde.kpresenter', 'kpr'],
+    ['application/vnd.kde.kspread', 'ksp'],
+    ['application/vnd.kde.kword', 'kwd'],
+    ['application/vnd.kenameaapp', 'htke'],
+    ['application/vnd.kidspiration', 'kia'],
+    ['application/vnd.kinar', 'kne'],
+    ['application/vnd.koan', 'skp'],
+    ['application/vnd.kodak-descriptor', 'sse'],
+    ['application/vnd.las.las+xml', 'lasxml'],
+    ['application/vnd.llamagraphics.life-balance.desktop', 'lbd'],
+    ['application/vnd.llamagraphics.life-balance.exchange+xml',
+        'lbe'
+    ],
+    ['application/vnd.lotus-1-2-3', '123'],
+    ['application/vnd.lotus-approach', 'apr'],
+    ['application/vnd.lotus-freelance', 'pre'],
+    ['application/vnd.lotus-notes', 'nsf'],
+    ['application/vnd.lotus-organizer', 'org'],
+    ['application/vnd.lotus-screencam', 'scm'],
+    ['application/vnd.lotus-wordpro', 'lwp'],
+    ['application/vnd.macports.portpkg', 'portpkg'],
+    ['application/vnd.mcd', 'mcd'],
+    ['application/vnd.medcalcdata', 'mc1'],
+    ['application/vnd.mediastation.cdkey', 'cdkey'],
+    ['application/vnd.mfer', 'mwf'],
+    ['application/vnd.mfmp', 'mfm'],
+    ['application/vnd.micrografx.flo', 'flo'],
+    ['application/vnd.micrografx.igx', 'igx'],
+    ['application/vnd.mif', 'mif'],
+    ['application/vnd.mobius.daf', 'daf'],
+    ['application/vnd.mobius.dis', 'dis'],
+    ['application/vnd.mobius.mbk', 'mbk'],
+    ['application/vnd.mobius.mqy', 'mqy'],
+    ['application/vnd.mobius.msl', 'msl'],
+    ['application/vnd.mobius.plc', 'plc'],
+    ['application/vnd.mobius.txf', 'txf'],
+    ['application/vnd.mophun.application', 'mpn'],
+    ['application/vnd.mophun.certificate', 'mpc'],
+    ['application/vnd.mozilla.xul+xml', 'xul'],
+    ['application/vnd.ms-artgalry', 'cil'],
+    ['application/vnd.ms-cab-compressed', 'cab'],
+    ['application/vnd.ms-excel', ['xls', 'xla', 'xlc', 'xlm', 'xlt', 'xlw', 'xlb', 'xll']],
+    ['application/vnd.ms-excel.addin.macroenabled.12', 'xlam'],
+    ['application/vnd.ms-excel.sheet.binary.macroenabled.12',
+        'xlsb'
+    ],
+    ['application/vnd.ms-excel.sheet.macroenabled.12', 'xlsm'],
+    ['application/vnd.ms-excel.template.macroenabled.12', 'xltm'],
+    ['application/vnd.ms-fontobject', 'eot'],
+    ['application/vnd.ms-htmlhelp', 'chm'],
+    ['application/vnd.ms-ims', 'ims'],
+    ['application/vnd.ms-lrm', 'lrm'],
+    ['application/vnd.ms-officetheme', 'thmx'],
+    ['application/vnd.ms-outlook', 'msg'],
+    ['application/vnd.ms-pki.certstore', 'sst'],
+    ['application/vnd.ms-pki.pko', 'pko'],
+    ['application/vnd.ms-pki.seccat', 'cat'],
+    ['application/vnd.ms-pki.stl', 'stl'],
+    ['application/vnd.ms-pkicertstore', 'sst'],
+    ['application/vnd.ms-pkiseccat', 'cat'],
+    ['application/vnd.ms-pkistl', 'stl'],
+    ['application/vnd.ms-powerpoint', ['ppt', 'pot', 'pps', 'ppa', 'pwz']],
+    ['application/vnd.ms-powerpoint.addin.macroenabled.12',
+        'ppam'
+    ],
+    ['application/vnd.ms-powerpoint.presentation.macroenabled.12',
+        'pptm'
+    ],
+    ['application/vnd.ms-powerpoint.slide.macroenabled.12',
+        'sldm'
+    ],
+    ['application/vnd.ms-powerpoint.slideshow.macroenabled.12',
+        'ppsm'
+    ],
+    ['application/vnd.ms-powerpoint.template.macroenabled.12',
+        'potm'
+    ],
+    ['application/vnd.ms-project', 'mpp'],
+    ['application/vnd.ms-word.document.macroenabled.12', 'docm'],
+    ['application/vnd.ms-word.template.macroenabled.12', 'dotm'],
+    ['application/vnd.ms-works', ['wks', 'wcm', 'wdb', 'wps']],
+    ['application/vnd.ms-wpl', 'wpl'],
+    ['application/vnd.ms-xpsdocument', 'xps'],
+    ['application/vnd.mseq', 'mseq'],
+    ['application/vnd.musician', 'mus'],
+    ['application/vnd.muvee.style', 'msty'],
+    ['application/vnd.neurolanguage.nlu', 'nlu'],
+    ['application/vnd.noblenet-directory', 'nnd'],
+    ['application/vnd.noblenet-sealer', 'nns'],
+    ['application/vnd.noblenet-web', 'nnw'],
+    ['application/vnd.nokia.configuration-message', 'ncm'],
+    ['application/vnd.nokia.n-gage.data', 'ngdat'],
+    ['application/vnd.nokia.n-gage.symbian.install', 'n-gage'],
+    ['application/vnd.nokia.radio-preset', 'rpst'],
+    ['application/vnd.nokia.radio-presets', 'rpss'],
+    ['application/vnd.nokia.ringing-tone', 'rng'],
+    ['application/vnd.novadigm.edm', 'edm'],
+    ['application/vnd.novadigm.edx', 'edx'],
+    ['application/vnd.novadigm.ext', 'ext'],
+    ['application/vnd.oasis.opendocument.chart', 'odc'],
+    ['application/vnd.oasis.opendocument.chart-template', 'otc'],
+    ['application/vnd.oasis.opendocument.database', 'odb'],
+    ['application/vnd.oasis.opendocument.formula', 'odf'],
+    ['application/vnd.oasis.opendocument.formula-template',
+        'odft'
+    ],
+    ['application/vnd.oasis.opendocument.graphics', 'odg'],
+    ['application/vnd.oasis.opendocument.graphics-template',
+        'otg'
+    ],
+    ['application/vnd.oasis.opendocument.image', 'odi'],
+    ['application/vnd.oasis.opendocument.image-template', 'oti'],
+    ['application/vnd.oasis.opendocument.presentation', 'odp'],
+    ['application/vnd.oasis.opendocument.presentation-template',
+        'otp'
+    ],
+    ['application/vnd.oasis.opendocument.spreadsheet', 'ods'],
+    ['application/vnd.oasis.opendocument.spreadsheet-template',
+        'ots'
+    ],
+    ['application/vnd.oasis.opendocument.text', 'odt'],
+    ['application/vnd.oasis.opendocument.text-master', 'odm'],
+    ['application/vnd.oasis.opendocument.text-template', 'ott'],
+    ['application/vnd.oasis.opendocument.text-web', 'oth'],
+    ['application/vnd.olpc-sugar', 'xo'],
+    ['application/vnd.oma.dd2+xml', 'dd2'],
+    ['application/vnd.openofficeorg.extension', 'oxt'],
+    ['application/vnd.openxmlformats-officedocument.presentationml.presentation',
+        'pptx'
+    ],
+    ['application/vnd.openxmlformats-officedocument.presentationml.slide',
+        'sldx'
+    ],
+    ['application/vnd.openxmlformats-officedocument.presentationml.slideshow',
+        'ppsx'
+    ],
+    ['application/vnd.openxmlformats-officedocument.presentationml.template',
+        'potx'
+    ],
+    ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+        'xlsx'
+    ],
+    ['application/vnd.openxmlformats-officedocument.spreadsheetml.template',
+        'xltx'
+    ],
+    ['application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+        'docx'
+    ],
+    ['application/vnd.openxmlformats-officedocument.wordprocessingml.template',
+        'dotx'
+    ],
+    ['application/vnd.osgeo.mapguide.package', 'mgp'],
+    ['application/vnd.osgi.dp', 'dp'],
+    ['application/vnd.palm', 'pdb'],
+    ['application/vnd.pawaafile', 'paw'],
+    ['application/vnd.pg.format', 'str'],
+    ['application/vnd.pg.osasli', 'ei6'],
+    ['application/vnd.picsel', 'efif'],
+    ['application/vnd.pmi.widget', 'wg'],
+    ['application/vnd.pocketlearn', 'plf'],
+    ['application/vnd.powerbuilder6', 'pbd'],
+    ['application/vnd.previewsystems.box', 'box'],
+    ['application/vnd.proteus.magazine', 'mgz'],
+    ['application/vnd.publishare-delta-tree', 'qps'],
+    ['application/vnd.pvi.ptid1', 'ptid'],
+    ['application/vnd.quark.quarkxpress', 'qxd'],
+    ['application/vnd.realvnc.bed', 'bed'],
+    ['application/vnd.recordare.musicxml', 'mxl'],
+    ['application/vnd.recordare.musicxml+xml', 'musicxml'],
+    ['application/vnd.rig.cryptonote', 'cryptonote'],
+    ['application/vnd.rim.cod', 'cod'],
+    ['application/vnd.rn-realmedia', 'rm'],
+    ['application/vnd.rn-realplayer', 'rnx'],
+    ['application/vnd.route66.link66+xml', 'link66'],
+    ['application/vnd.sailingtracker.track', 'st'],
+    ['application/vnd.seemail', 'see'],
+    ['application/vnd.sema', 'sema'],
+    ['application/vnd.semd', 'semd'],
+    ['application/vnd.semf', 'semf'],
+    ['application/vnd.shana.informed.formdata', 'ifm'],
+    ['application/vnd.shana.informed.formtemplate', 'itp'],
+    ['application/vnd.shana.informed.interchange', 'iif'],
+    ['application/vnd.shana.informed.package', 'ipk'],
+    ['application/vnd.simtech-mindmapper', 'twd'],
+    ['application/vnd.smaf', 'mmf'],
+    ['application/vnd.smart.teacher', 'teacher'],
+    ['application/vnd.solent.sdkm+xml', 'sdkm'],
+    ['application/vnd.spotfire.dxp', 'dxp'],
+    ['application/vnd.spotfire.sfs', 'sfs'],
+    ['application/vnd.stardivision.calc', 'sdc'],
+    ['application/vnd.stardivision.draw', 'sda'],
+    ['application/vnd.stardivision.impress', 'sdd'],
+    ['application/vnd.stardivision.math', 'smf'],
+    ['application/vnd.stardivision.writer', 'sdw'],
+    ['application/vnd.stardivision.writer-global', 'sgl'],
+    ['application/vnd.stepmania.stepchart', 'sm'],
+    ['application/vnd.sun.xml.calc', 'sxc'],
+    ['application/vnd.sun.xml.calc.template', 'stc'],
+    ['application/vnd.sun.xml.draw', 'sxd'],
+    ['application/vnd.sun.xml.draw.template', 'std'],
+    ['application/vnd.sun.xml.impress', 'sxi'],
+    ['application/vnd.sun.xml.impress.template', 'sti'],
+    ['application/vnd.sun.xml.math', 'sxm'],
+    ['application/vnd.sun.xml.writer', 'sxw'],
+    ['application/vnd.sun.xml.writer.global', 'sxg'],
+    ['application/vnd.sun.xml.writer.template', 'stw'],
+    ['application/vnd.sus-calendar', 'sus'],
+    ['application/vnd.svd', 'svd'],
+    ['application/vnd.symbian.install', 'sis'],
+    ['application/vnd.syncml+xml', 'xsm'],
+    ['application/vnd.syncml.dm+wbxml', 'bdm'],
+    ['application/vnd.syncml.dm+xml', 'xdm'],
+    ['application/vnd.tao.intent-module-archive', 'tao'],
+    ['application/vnd.tmobile-livetv', 'tmo'],
+    ['application/vnd.trid.tpt', 'tpt'],
+    ['application/vnd.triscape.mxs', 'mxs'],
+    ['application/vnd.trueapp', 'tra'],
+    ['application/vnd.ufdl', 'ufd'],
+    ['application/vnd.uiq.theme', 'utz'],
+    ['application/vnd.umajin', 'umj'],
+    ['application/vnd.unity', 'unityweb'],
+    ['application/vnd.uoml+xml', 'uoml'],
+    ['application/vnd.vcx', 'vcx'],
+    ['application/vnd.visio', 'vsd'],
+    ['application/vnd.visionary', 'vis'],
+    ['application/vnd.vsf', 'vsf'],
+    ['application/vnd.wap.wbxml', 'wbxml'],
+    ['application/vnd.wap.wmlc', 'wmlc'],
+    ['application/vnd.wap.wmlscriptc', 'wmlsc'],
+    ['application/vnd.webturbo', 'wtb'],
+    ['application/vnd.wolfram.player', 'nbp'],
+    ['application/vnd.wordperfect', 'wpd'],
+    ['application/vnd.wqd', 'wqd'],
+    ['application/vnd.wt.stf', 'stf'],
+    ['application/vnd.xara', ['web', 'xar']],
+    ['application/vnd.xfdl', 'xfdl'],
+    ['application/vnd.yamaha.hv-dic', 'hvd'],
+    ['application/vnd.yamaha.hv-script', 'hvs'],
+    ['application/vnd.yamaha.hv-voice', 'hvp'],
+    ['application/vnd.yamaha.openscoreformat', 'osf'],
+    ['application/vnd.yamaha.openscoreformat.osfpvg+xml',
+        'osfpvg'
+    ],
+    ['application/vnd.yamaha.smaf-audio', 'saf'],
+    ['application/vnd.yamaha.smaf-phrase', 'spf'],
+    ['application/vnd.yellowriver-custom-menu', 'cmp'],
+    ['application/vnd.zul', 'zir'],
+    ['application/vnd.zzazz.deck+xml', 'zaz'],
+    ['application/vocaltec-media-desc', 'vmd'],
+    ['application/vocaltec-media-file', 'vmf'],
+    ['application/voicexml+xml', 'vxml'],
+    ['application/widget', 'wgt'],
+    ['application/winhlp', 'hlp'],
+    ['application/wordperfect', ['wp', 'wp5', 'wp6', 'wpd']],
+    ['application/wordperfect6.0', ['w60', 'wp5']],
+    ['application/wordperfect6.1', 'w61'],
+    ['application/wsdl+xml', 'wsdl'],
+    ['application/wspolicy+xml', 'wspolicy'],
+    ['application/x-123', 'wk1'],
+    ['application/x-7z-compressed', '7z'],
+    ['application/x-abiword', 'abw'],
+    ['application/x-ace-compressed', 'ace'],
+    ['application/x-aim', 'aim'],
+    ['application/x-authorware-bin', 'aab'],
+    ['application/x-authorware-map', 'aam'],
+    ['application/x-authorware-seg', 'aas'],
+    ['application/x-bcpio', 'bcpio'],
+    ['application/x-binary', 'bin'],
+    ['application/x-binhex40', 'hqx'],
+    ['application/x-bittorrent', 'torrent'],
+    ['application/x-bsh', ['bsh', 'sh', 'shar']],
+    ['application/x-bytecode.elisp', 'elc'],
+    ['applicaiton/x-bytecode.python', 'pyc'],
+    ['application/x-bzip', 'bz'],
+    ['application/x-bzip2', ['boz', 'bz2']],
+    ['application/x-cdf', 'cdf'],
+    ['application/x-cdlink', 'vcd'],
+    ['application/x-chat', ['cha', 'chat']],
+    ['application/x-chess-pgn', 'pgn'],
+    ['application/x-cmu-raster', 'ras'],
+    ['application/x-cocoa', 'cco'],
+    ['application/x-compactpro', 'cpt'],
+    ['application/x-compress', 'z'],
+    ['application/x-compressed', ['tgz', 'gz', 'z', 'zip']],
+    ['application/x-conference', 'nsc'],
+    ['application/x-cpio', 'cpio'],
+    ['application/x-cpt', 'cpt'],
+    ['application/x-csh', 'csh'],
+    ['application/x-debian-package', 'deb'],
+    ['application/x-deepv', 'deepv'],
+    ['application/x-director', ['dir', 'dcr', 'dxr']],
+    ['application/x-doom', 'wad'],
+    ['application/x-dtbncx+xml', 'ncx'],
+    ['application/x-dtbook+xml', 'dtb'],
+    ['application/x-dtbresource+xml', 'res'],
+    ['application/x-dvi', 'dvi'],
+    ['application/x-elc', 'elc'],
+    ['application/x-envoy', ['env', 'evy']],
+    ['application/x-esrehber', 'es'],
+    ['application/x-excel', ['xls',
+        'xla',
+        'xlb',
+        'xlc',
+        'xld',
+        'xlk',
+        'xll',
+        'xlm',
+        'xlt',
+        'xlv',
+        'xlw'
+    ]],
+    ['application/x-font-bdf', 'bdf'],
+    ['application/x-font-ghostscript', 'gsf'],
+    ['application/x-font-linux-psf', 'psf'],
+    ['application/x-font-otf', 'otf'],
+    ['application/x-font-pcf', 'pcf'],
+    ['application/x-font-snf', 'snf'],
+    ['application/x-font-ttf', 'ttf'],
+    ['application/x-font-type1', 'pfa'],
+    ['application/x-font-woff', 'woff'],
+    ['application/x-frame', 'mif'],
+    ['application/x-freelance', 'pre'],
+    ['application/x-futuresplash', 'spl'],
+    ['application/x-gnumeric', 'gnumeric'],
+    ['application/x-gsp', 'gsp'],
+    ['application/x-gss', 'gss'],
+    ['application/x-gtar', 'gtar'],
+    ['application/x-gzip', ['gz', 'gzip']],
+    ['application/x-hdf', 'hdf'],
+    ['application/x-helpfile', ['help', 'hlp']],
+    ['application/x-httpd-imap', 'imap'],
+    ['application/x-ima', 'ima'],
+    ['application/x-internet-signup', ['ins', 'isp']],
+    ['application/x-internett-signup', 'ins'],
+    ['application/x-inventor', 'iv'],
+    ['application/x-ip2', 'ip'],
+    ['application/x-iphone', 'iii'],
+    ['application/x-java-class', 'class'],
+    ['application/x-java-commerce', 'jcm'],
+    ['application/x-java-jnlp-file', 'jnlp'],
+    ['application/x-javascript', 'js'],
+    ['application/x-koan', ['skd', 'skm', 'skp', 'skt']],
+    ['application/x-ksh', 'ksh'],
+    ['application/x-latex', ['latex', 'ltx']],
+    ['application/x-lha', 'lha'],
+    ['application/x-lisp', 'lsp'],
+    ['application/x-livescreen', 'ivy'],
+    ['application/x-lotus', 'wq1'],
+    ['application/x-lotusscreencam', 'scm'],
+    ['application/x-lzh', 'lzh'],
+    ['application/x-lzx', 'lzx'],
+    ['application/x-mac-binhex40', 'hqx'],
+    ['application/x-macbinary', 'bin'],
+    ['application/x-magic-cap-package-1.0', 'mc$'],
+    ['application/x-mathcad', 'mcd'],
+    ['application/x-meme', 'mm'],
+    ['application/x-midi', ['mid', 'midi']],
+    ['application/x-mif', 'mif'],
+    ['application/x-mix-transfer', 'nix'],
+    ['application/x-mobipocket-ebook', 'prc'],
+    ['application/x-mplayer2', 'asx'],
+    ['application/x-ms-application', 'application'],
+    ['application/x-ms-wmd', 'wmd'],
+    ['application/x-ms-wmz', 'wmz'],
+    ['application/x-ms-xbap', 'xbap'],
+    ['application/x-msaccess', 'mdb'],
+    ['application/x-msbinder', 'obd'],
+    ['application/x-mscardfile', 'crd'],
+    ['application/x-msclip', 'clp'],
+    ['application/x-msdownload', ['exe', 'dll']],
+    ['application/x-msexcel', ['xls', 'xla', 'xlw']],
+    ['application/x-msmediaview', ['mvb', 'm13', 'm14']],
+    ['application/x-msmetafile', 'wmf'],
+    ['application/x-msmoney', 'mny'],
+    ['application/x-mspowerpoint', 'ppt'],
+    ['application/x-mspublisher', 'pub'],
+    ['application/x-msschedule', 'scd'],
+    ['application/x-msterminal', 'trm'],
+    ['application/x-mswrite', 'wri'],
+    ['application/x-navi-animation', 'ani'],
+    ['application/x-navidoc', 'nvd'],
+    ['application/x-navimap', 'map'],
+    ['application/x-navistyle', 'stl'],
+    ['application/x-netcdf', ['cdf', 'nc']],
+    ['application/x-newton-compatible-pkg', 'pkg'],
+    ['application/x-nokia-9000-communicator-add-on-software',
+        'aos'
+    ],
+    ['application/x-omc', 'omc'],
+    ['application/x-omcdatamaker', 'omcd'],
+    ['application/x-omcregerator', 'omcr'],
+    ['application/x-pagemaker', ['pm4', 'pm5']],
+    ['application/x-pcl', 'pcl'],
+    ['application/x-perfmon', ['pma', 'pmc', 'pml', 'pmr', 'pmw']],
+    ['application/x-pixclscript', 'plx'],
+    ['application/x-pkcs10', 'p10'],
+    ['application/x-pkcs12', ['p12', 'pfx']],
+    ['application/x-pkcs7-certificates', ['p7b', 'spc']],
+    ['application/x-pkcs7-certreqresp', 'p7r'],
+    ['application/x-pkcs7-mime', ['p7m', 'p7c']],
+    ['application/x-pkcs7-signature', ['p7s', 'p7a']],
+    ['application/x-pointplus', 'css'],
+    ['application/x-portable-anymap', 'pnm'],
+    ['application/x-project', ['mpc', 'mpt', 'mpv', 'mpx']],
+    ['application/x-qpro', 'wb1'],
+    ['application/x-rar-compressed', 'rar'],
+    ['application/x-rtf', 'rtf'],
+    ['application/x-sdp', 'sdp'],
+    ['application/x-sea', 'sea'],
+    ['application/x-seelogo', 'sl'],
+    ['application/x-sh', 'sh'],
+    ['application/x-shar', ['shar', 'sh']],
+    ['application/x-shockwave-flash', 'swf'],
+    ['application/x-silverlight-app', 'xap'],
+    ['application/x-sit', 'sit'],
+    ['application/x-sprite', ['spr', 'sprite']],
+    ['application/x-stuffit', 'sit'],
+    ['application/x-stuffitx', 'sitx'],
+    ['application/x-sv4cpio', 'sv4cpio'],
+    ['application/x-sv4crc', 'sv4crc'],
+    ['application/x-tar', 'tar'],
+    ['application/x-tbook', ['sbk', 'tbk']],
+    ['application/x-tcl', 'tcl'],
+    ['application/x-tex', 'tex'],
+    ['application/x-tex-tfm', 'tfm'],
+    ['application/x-texinfo', ['texi', 'texinfo']],
+    ['application/x-troff', ['roff', 't', 'tr']],
+    ['application/x-troff-man', 'man'],
+    ['application/x-troff-me', 'me'],
+    ['application/x-troff-ms', 'ms'],
+    ['application/x-troff-msvideo', 'avi'],
+    ['application/x-ustar', 'ustar'],
+    ['application/x-visio', ['vsd', 'vst', 'vsw']],
+    ['application/x-vnd.audioexplosion.mzz', 'mzz'],
+    ['application/x-vnd.ls-xpix', 'xpix'],
+    ['application/x-vrml', 'vrml'],
+    ['application/x-wais-source', ['src', 'wsrc']],
+    ['application/x-winhelp', 'hlp'],
+    ['application/x-wintalk', 'wtk'],
+    ['application/x-world', ['wrl', 'svr']],
+    ['application/x-wpwin', 'wpd'],
+    ['application/x-wri', 'wri'],
+    ['application/x-x509-ca-cert', ['cer', 'crt', 'der']],
+    ['application/x-x509-user-cert', 'crt'],
+    ['application/x-xfig', 'fig'],
+    ['application/x-xpinstall', 'xpi'],
+    ['application/x-zip-compressed', 'zip'],
+    ['application/xcap-diff+xml', 'xdf'],
+    ['application/xenc+xml', 'xenc'],
+    ['application/xhtml+xml', 'xhtml'],
+    ['application/xml', 'xml'],
+    ['application/xml-dtd', 'dtd'],
+    ['application/xop+xml', 'xop'],
+    ['application/xslt+xml', 'xslt'],
+    ['application/xspf+xml', 'xspf'],
+    ['application/xv+xml', 'mxml'],
+    ['application/yang', 'yang'],
+    ['application/yin+xml', 'yin'],
+    ['application/ynd.ms-pkipko', 'pko'],
+    ['application/zip', 'zip'],
+    ['audio/adpcm', 'adp'],
+    ['audio/aiff', ['aiff', 'aif', 'aifc']],
+    ['audio/basic', ['snd', 'au']],
+    ['audio/it', 'it'],
+    ['audio/make', ['funk', 'my', 'pfunk']],
+    ['audio/make.my.funk', 'pfunk'],
+    ['audio/mid', ['mid', 'rmi']],
+    ['audio/midi', ['midi', 'kar', 'mid']],
+    ['audio/mod', 'mod'],
+    ['audio/mp4', 'mp4a'],
+    ['audio/mpeg', ['mpga', 'mp3', 'm2a', 'mp2', 'mpa', 'mpg']],
+    ['audio/mpeg3', 'mp3'],
+    ['audio/nspaudio', ['la', 'lma']],
+    ['audio/ogg', 'oga'],
+    ['audio/s3m', 's3m'],
+    ['audio/tsp-audio', 'tsi'],
+    ['audio/tsplayer', 'tsp'],
+    ['audio/vnd.dece.audio', 'uva'],
+    ['audio/vnd.digital-winds', 'eol'],
+    ['audio/vnd.dra', 'dra'],
+    ['audio/vnd.dts', 'dts'],
+    ['audio/vnd.dts.hd', 'dtshd'],
+    ['audio/vnd.lucent.voice', 'lvp'],
+    ['audio/vnd.ms-playready.media.pya', 'pya'],
+    ['audio/vnd.nuera.ecelp4800', 'ecelp4800'],
+    ['audio/vnd.nuera.ecelp7470', 'ecelp7470'],
+    ['audio/vnd.nuera.ecelp9600', 'ecelp9600'],
+    ['audio/vnd.qcelp', 'qcp'],
+    ['audio/vnd.rip', 'rip'],
+    ['audio/voc', 'voc'],
+    ['audio/voxware', 'vox'],
+    ['audio/wav', 'wav'],
+    ['audio/webm', 'weba'],
+    ['audio/x-aac', 'aac'],
+    ['audio/x-adpcm', 'snd'],
+    ['audio/x-aiff', ['aiff', 'aif', 'aifc']],
+    ['audio/x-au', 'au'],
+    ['audio/x-gsm', ['gsd', 'gsm']],
+    ['audio/x-jam', 'jam'],
+    ['audio/x-liveaudio', 'lam'],
+    ['audio/x-mid', ['mid', 'midi']],
+    ['audio/x-midi', ['midi', 'mid']],
+    ['audio/x-mod', 'mod'],
+    ['audio/x-mpeg', 'mp2'],
+    ['audio/x-mpeg-3', 'mp3'],
+    ['audio/x-mpegurl', 'm3u'],
+    ['audio/x-mpequrl', 'm3u'],
+    ['audio/x-ms-wax', 'wax'],
+    ['audio/x-ms-wma', 'wma'],
+    ['audio/x-nspaudio', ['la', 'lma']],
+    ['audio/x-pn-realaudio', ['ra', 'ram', 'rm', 'rmm', 'rmp']],
+    ['audio/x-pn-realaudio-plugin', ['ra', 'rmp', 'rpm']],
+    ['audio/x-psid', 'sid'],
+    ['audio/x-realaudio', 'ra'],
+    ['audio/x-twinvq', 'vqf'],
+    ['audio/x-twinvq-plugin', ['vqe', 'vql']],
+    ['audio/x-vnd.audioexplosion.mjuicemediafile', 'mjf'],
+    ['audio/x-voc', 'voc'],
+    ['audio/x-wav', 'wav'],
+    ['audio/xm', 'xm'],
+    ['chemical/x-cdx', 'cdx'],
+    ['chemical/x-cif', 'cif'],
+    ['chemical/x-cmdf', 'cmdf'],
+    ['chemical/x-cml', 'cml'],
+    ['chemical/x-csml', 'csml'],
+    ['chemical/x-pdb', ['pdb', 'xyz']],
+    ['chemical/x-xyz', 'xyz'],
+    ['drawing/x-dwf', 'dwf'],
+    ['i-world/i-vrml', 'ivr'],
+    ['image/bmp', ['bmp', 'bm']],
+    ['image/cgm', 'cgm'],
+    ['image/cis-cod', 'cod'],
+    ['image/cmu-raster', ['ras', 'rast']],
+    ['image/fif', 'fif'],
+    ['image/florian', ['flo', 'turbot']],
+    ['image/g3fax', 'g3'],
+    ['image/gif', 'gif'],
+    ['image/ief', ['ief', 'iefs']],
+    ['image/jpeg', ['jpeg', 'jpe', 'jpg', 'jfif', 'jfif-tbnl']],
+    ['image/jutvision', 'jut'],
+    ['image/ktx', 'ktx'],
+    ['image/naplps', ['nap', 'naplps']],
+    ['image/pict', ['pic', 'pict']],
+    ['image/pipeg', 'jfif'],
+    ['image/pjpeg', ['jfif', 'jpe', 'jpeg', 'jpg']],
+    ['image/png', ['png', 'x-png']],
+    ['image/prs.btif', 'btif'],
+    ['image/svg+xml', 'svg'],
+    ['image/tiff', ['tif', 'tiff']],
+    ['image/vasa', 'mcf'],
+    ['image/vnd.adobe.photoshop', 'psd'],
+    ['image/vnd.dece.graphic', 'uvi'],
+    ['image/vnd.djvu', 'djvu'],
+    ['image/vnd.dvb.subtitle', 'sub'],
+    ['image/vnd.dwg', ['dwg', 'dxf', 'svf']],
+    ['image/vnd.dxf', 'dxf'],
+    ['image/vnd.fastbidsheet', 'fbs'],
+    ['image/vnd.fpx', 'fpx'],
+    ['image/vnd.fst', 'fst'],
+    ['image/vnd.fujixerox.edmics-mmr', 'mmr'],
+    ['image/vnd.fujixerox.edmics-rlc', 'rlc'],
+    ['image/vnd.ms-modi', 'mdi'],
+    ['image/vnd.net-fpx', ['fpx', 'npx']],
+    ['image/vnd.rn-realflash', 'rf'],
+    ['image/vnd.rn-realpix', 'rp'],
+    ['image/vnd.wap.wbmp', 'wbmp'],
+    ['image/vnd.xiff', 'xif'],
+    ['image/webp', 'webp'],
+    ['image/x-cmu-raster', 'ras'],
+    ['image/x-cmx', 'cmx'],
+    ['image/x-dwg', ['dwg', 'dxf', 'svf']],
+    ['image/x-freehand', 'fh'],
+    ['image/x-icon', 'ico'],
+    ['image/x-jg', 'art'],
+    ['image/x-jps', 'jps'],
+    ['image/x-niff', ['niff', 'nif']],
+    ['image/x-pcx', 'pcx'],
+    ['image/x-pict', ['pct', 'pic']],
+    ['image/x-portable-anymap', 'pnm'],
+    ['image/x-portable-bitmap', 'pbm'],
+    ['image/x-portable-graymap', 'pgm'],
+    ['image/x-portable-greymap', 'pgm'],
+    ['image/x-portable-pixmap', 'ppm'],
+    ['image/x-quicktime', ['qif', 'qti', 'qtif']],
+    ['image/x-rgb', 'rgb'],
+    ['image/x-tiff', ['tif', 'tiff']],
+    ['image/x-windows-bmp', 'bmp'],
+    ['image/x-xbitmap', 'xbm'],
+    ['image/x-xbm', 'xbm'],
+    ['image/x-xpixmap', ['xpm', 'pm']],
+    ['image/x-xwd', 'xwd'],
+    ['image/x-xwindowdump', 'xwd'],
+    ['image/xbm', 'xbm'],
+    ['image/xpm', 'xpm'],
+    ['message/rfc822', ['eml', 'mht', 'mhtml', 'nws', 'mime']],
+    ['model/iges', ['iges', 'igs']],
+    ['model/mesh', 'msh'],
+    ['model/vnd.collada+xml', 'dae'],
+    ['model/vnd.dwf', 'dwf'],
+    ['model/vnd.gdl', 'gdl'],
+    ['model/vnd.gtw', 'gtw'],
+    ['model/vnd.mts', 'mts'],
+    ['model/vnd.vtu', 'vtu'],
+    ['model/vrml', ['vrml', 'wrl', 'wrz']],
+    ['model/x-pov', 'pov'],
+    ['multipart/x-gzip', 'gzip'],
+    ['multipart/x-ustar', 'ustar'],
+    ['multipart/x-zip', 'zip'],
+    ['music/crescendo', ['mid', 'midi']],
+    ['music/x-karaoke', 'kar'],
+    ['paleovu/x-pv', 'pvu'],
+    ['text/asp', 'asp'],
+    ['text/calendar', 'ics'],
+    ['text/css', 'css'],
+    ['text/csv', 'csv'],
+    ['text/ecmascript', 'js'],
+    ['text/h323', '323'],
+    ['text/html', ['html', 'htm', 'stm', 'acgi', 'htmls', 'htx', 'shtml']],
+    ['text/iuls', 'uls'],
+    ['text/javascript', 'js'],
+    ['text/mcf', 'mcf'],
+    ['text/n3', 'n3'],
+    ['text/pascal', 'pas'],
+    ['text/plain', ['txt',
+        'bas',
+        'c',
+        'h',
+        'c++',
+        'cc',
+        'com',
+        'conf',
+        'cxx',
+        'def',
+        'f',
+        'f90',
+        'for',
+        'g',
+        'hh',
+        'idc',
+        'jav',
+        'java',
+        'list',
+        'log',
+        'lst',
+        'm',
+        'mar',
+        'pl',
+        'sdml',
+        'text'
+    ]],
+    ['text/plain-bas', 'par'],
+    ['text/prs.lines.tag', 'dsc'],
+    ['text/richtext', ['rtx', 'rt', 'rtf']],
+    ['text/scriplet', 'wsc'],
+    ['text/scriptlet', 'sct'],
+    ['text/sgml', ['sgm', 'sgml']],
+    ['text/tab-separated-values', 'tsv'],
+    ['text/troff', 't'],
+    ['text/turtle', 'ttl'],
+    ['text/uri-list', ['uni', 'unis', 'uri', 'uris']],
+    ['text/vnd.abc', 'abc'],
+    ['text/vnd.curl', 'curl'],
+    ['text/vnd.curl.dcurl', 'dcurl'],
+    ['text/vnd.curl.mcurl', 'mcurl'],
+    ['text/vnd.curl.scurl', 'scurl'],
+    ['text/vnd.fly', 'fly'],
+    ['text/vnd.fmi.flexstor', 'flx'],
+    ['text/vnd.graphviz', 'gv'],
+    ['text/vnd.in3d.3dml', '3dml'],
+    ['text/vnd.in3d.spot', 'spot'],
+    ['text/vnd.rn-realtext', 'rt'],
+    ['text/vnd.sun.j2me.app-descriptor', 'jad'],
+    ['text/vnd.wap.wml', 'wml'],
+    ['text/vnd.wap.wmlscript', 'wmls'],
+    ['text/webviewhtml', 'htt'],
+    ['text/x-asm', ['asm', 's']],
+    ['text/x-audiosoft-intra', 'aip'],
+    ['text/x-c', ['c', 'cc', 'cpp']],
+    ['text/x-component', 'htc'],
+    ['text/x-fortran', ['for', 'f', 'f77', 'f90']],
+    ['text/x-h', ['h', 'hh']],
+    ['text/x-java-source', ['java', 'jav']],
+    ['text/x-java-source,java', 'java'],
+    ['text/x-la-asf', 'lsx'],
+    ['text/x-m', 'm'],
+    ['text/x-pascal', 'p'],
+    ['text/x-script', 'hlb'],
+    ['text/x-script.csh', 'csh'],
+    ['text/x-script.elisp', 'el'],
+    ['text/x-script.guile', 'scm'],
+    ['text/x-script.ksh', 'ksh'],
+    ['text/x-script.lisp', 'lsp'],
+    ['text/x-script.perl', 'pl'],
+    ['text/x-script.perl-module', 'pm'],
+    ['text/x-script.phyton', 'py'],
+    ['text/x-script.rexx', 'rexx'],
+    ['text/x-script.scheme', 'scm'],
+    ['text/x-script.sh', 'sh'],
+    ['text/x-script.tcl', 'tcl'],
+    ['text/x-script.tcsh', 'tcsh'],
+    ['text/x-script.zsh', 'zsh'],
+    ['text/x-server-parsed-html', ['shtml', 'ssi']],
+    ['text/x-setext', 'etx'],
+    ['text/x-sgml', ['sgm', 'sgml']],
+    ['text/x-speech', ['spc', 'talk']],
+    ['text/x-uil', 'uil'],
+    ['text/x-uuencode', ['uu', 'uue']],
+    ['text/x-vcalendar', 'vcs'],
+    ['text/x-vcard', 'vcf'],
+    ['text/xml', 'xml'],
+    ['video/3gpp', '3gp'],
+    ['video/3gpp2', '3g2'],
+    ['video/animaflex', 'afl'],
+    ['video/avi', 'avi'],
+    ['video/avs-video', 'avs'],
+    ['video/dl', 'dl'],
+    ['video/fli', 'fli'],
+    ['video/gl', 'gl'],
+    ['video/h261', 'h261'],
+    ['video/h263', 'h263'],
+    ['video/h264', 'h264'],
+    ['video/jpeg', 'jpgv'],
+    ['video/jpm', 'jpm'],
+    ['video/mj2', 'mj2'],
+    ['video/mp4', 'mp4'],
+    ['video/mpeg', ['mpeg', 'mp2', 'mpa', 'mpe', 'mpg', 'mpv2', 'm1v', 'm2v', 'mp3']],
+    ['video/msvideo', 'avi'],
+    ['video/ogg', 'ogv'],
+    ['video/quicktime', ['mov', 'qt', 'moov']],
+    ['video/vdo', 'vdo'],
+    ['video/vivo', ['viv', 'vivo']],
+    ['video/vnd.dece.hd', 'uvh'],
+    ['video/vnd.dece.mobile', 'uvm'],
+    ['video/vnd.dece.pd', 'uvp'],
+    ['video/vnd.dece.sd', 'uvs'],
+    ['video/vnd.dece.video', 'uvv'],
+    ['video/vnd.fvt', 'fvt'],
+    ['video/vnd.mpegurl', 'mxu'],
+    ['video/vnd.ms-playready.media.pyv', 'pyv'],
+    ['video/vnd.rn-realvideo', 'rv'],
+    ['video/vnd.uvvu.mp4', 'uvu'],
+    ['video/vnd.vivo', ['viv', 'vivo']],
+    ['video/vosaic', 'vos'],
+    ['video/webm', 'webm'],
+    ['video/x-amt-demorun', 'xdr'],
+    ['video/x-amt-showrun', 'xsr'],
+    ['video/x-atomic3d-feature', 'fmf'],
+    ['video/x-dl', 'dl'],
+    ['video/x-dv', ['dif', 'dv']],
+    ['video/x-f4v', 'f4v'],
+    ['video/x-fli', 'fli'],
+    ['video/x-flv', 'flv'],
+    ['video/x-gl', 'gl'],
+    ['video/x-isvideo', 'isu'],
+    ['video/x-la-asf', ['lsf', 'lsx']],
+    ['video/x-m4v', 'm4v'],
+    ['video/x-motion-jpeg', 'mjpg'],
+    ['video/x-mpeg', ['mp3', 'mp2']],
+    ['video/x-mpeq2a', 'mp2'],
+    ['video/x-ms-asf', ['asf', 'asr', 'asx']],
+    ['video/x-ms-asf-plugin', 'asx'],
+    ['video/x-ms-wm', 'wm'],
+    ['video/x-ms-wmv', 'wmv'],
+    ['video/x-ms-wmx', 'wmx'],
+    ['video/x-ms-wvx', 'wvx'],
+    ['video/x-msvideo', 'avi'],
+    ['video/x-qtc', 'qtc'],
+    ['video/x-scm', 'scm'],
+    ['video/x-sgi-movie', ['movie', 'mv']],
+    ['windows/metafile', 'wmf'],
+    ['www/mime', 'mime'],
+    ['x-conference/x-cooltalk', 'ice'],
+    ['x-music/x-midi', ['mid', 'midi']],
+    ['x-world/x-3dmf', ['3dm', '3dmf', 'qd3', 'qd3d']],
+    ['x-world/x-svr', 'svr'],
+    ['x-world/x-vrml', ['flr', 'vrml', 'wrl', 'wrz', 'xaf', 'xof']],
+    ['x-world/x-vrt', 'vrt'],
+    ['xgl/drawing', 'xgz'],
+    ['xgl/movie', 'xmz']
+]);
+const extensions = new Map([
+    ['123', 'application/vnd.lotus-1-2-3'],
+    ['323', 'text/h323'],
+    ['*', 'application/octet-stream'],
+    ['3dm', 'x-world/x-3dmf'],
+    ['3dmf', 'x-world/x-3dmf'],
+    ['3dml', 'text/vnd.in3d.3dml'],
+    ['3g2', 'video/3gpp2'],
+    ['3gp', 'video/3gpp'],
+    ['7z', 'application/x-7z-compressed'],
+    ['a', 'application/octet-stream'],
+    ['aab', 'application/x-authorware-bin'],
+    ['aac', 'audio/x-aac'],
+    ['aam', 'application/x-authorware-map'],
+    ['aas', 'application/x-authorware-seg'],
+    ['abc', 'text/vnd.abc'],
+    ['abw', 'application/x-abiword'],
+    ['ac', 'application/pkix-attr-cert'],
+    ['acc', 'application/vnd.americandynamics.acc'],
+    ['ace', 'application/x-ace-compressed'],
+    ['acgi', 'text/html'],
+    ['acu', 'application/vnd.acucobol'],
+    ['acx', 'application/internet-property-stream'],
+    ['adp', 'audio/adpcm'],
+    ['aep', 'application/vnd.audiograph'],
+    ['afl', 'video/animaflex'],
+    ['afp', 'application/vnd.ibm.modcap'],
+    ['ahead', 'application/vnd.ahead.space'],
+    ['ai', 'application/postscript'],
+    ['aif', ['audio/aiff', 'audio/x-aiff']],
+    ['aifc', ['audio/aiff', 'audio/x-aiff']],
+    ['aiff', ['audio/aiff', 'audio/x-aiff']],
+    ['aim', 'application/x-aim'],
+    ['aip', 'text/x-audiosoft-intra'],
+    ['air',
+        'application/vnd.adobe.air-application-installer-package+zip'
+    ],
+    ['ait', 'application/vnd.dvb.ait'],
+    ['ami', 'application/vnd.amiga.ami'],
+    ['ani', 'application/x-navi-animation'],
+    ['aos',
+        'application/x-nokia-9000-communicator-add-on-software'
+    ],
+    ['apk', 'application/vnd.android.package-archive'],
+    ['application', 'application/x-ms-application'],
+    ['apr', 'application/vnd.lotus-approach'],
+    ['aps', 'application/mime'],
+    ['arc', 'application/octet-stream'],
+    ['arj', ['application/arj', 'application/octet-stream']],
+    ['art', 'image/x-jg'],
+    ['asf', 'video/x-ms-asf'],
+    ['asm', 'text/x-asm'],
+    ['aso', 'application/vnd.accpac.simply.aso'],
+    ['asp', 'text/asp'],
+    ['asr', 'video/x-ms-asf'],
+    ['asx', ['video/x-ms-asf',
+        'application/x-mplayer2',
+        'video/x-ms-asf-plugin'
+    ]],
+    ['atc', 'application/vnd.acucorp'],
+    ['atomcat', 'application/atomcat+xml'],
+    ['atomsvc', 'application/atomsvc+xml'],
+    ['atx', 'application/vnd.antix.game-component'],
+    ['au', ['audio/basic', 'audio/x-au']],
+    ['avi', ['video/avi',
+        'video/msvideo',
+        'application/x-troff-msvideo',
+        'video/x-msvideo'
+    ]],
+    ['avs', 'video/avs-video'],
+    ['aw', 'application/applixware'],
+    ['axs', 'application/olescript'],
+    ['azf', 'application/vnd.airzip.filesecure.azf'],
+    ['azs', 'application/vnd.airzip.filesecure.azs'],
+    ['azw', 'application/vnd.amazon.ebook'],
+    ['bas', 'text/plain'],
+    ['bcpio', 'application/x-bcpio'],
+    ['bdf', 'application/x-font-bdf'],
+    ['bdm', 'application/vnd.syncml.dm+wbxml'],
+    ['bed', 'application/vnd.realvnc.bed'],
+    ['bh2', 'application/vnd.fujitsu.oasysprs'],
+    ['bin', ['application/octet-stream',
+        'application/mac-binary',
+        'application/macbinary',
+        'application/x-macbinary',
+        'application/x-binary'
+    ]],
+    ['bm', 'image/bmp'],
+    ['bmi', 'application/vnd.bmi'],
+    ['bmp', ['image/bmp', 'image/x-windows-bmp']],
+    ['boo', 'application/book'],
+    ['book', 'application/book'],
+    ['box', 'application/vnd.previewsystems.box'],
+    ['boz', 'application/x-bzip2'],
+    ['bsh', 'application/x-bsh'],
+    ['btif', 'image/prs.btif'],
+    ['bz', 'application/x-bzip'],
+    ['bz2', 'application/x-bzip2'],
+    ['c', ['text/plain', 'text/x-c']],
+    ['c++', 'text/plain'],
+    ['c11amc', 'application/vnd.cluetrust.cartomobile-config'],
+    ['c11amz', 'application/vnd.cluetrust.cartomobile-config-pkg'],
+    ['c4g', 'application/vnd.clonk.c4group'],
+    ['cab', 'application/vnd.ms-cab-compressed'],
+    ['car', 'application/vnd.curl.car'],
+    ['cat', ['application/vnd.ms-pkiseccat',
+        'application/vnd.ms-pki.seccat'
+    ]],
+    ['cc', ['text/plain', 'text/x-c']],
+    ['ccad', 'application/clariscad'],
+    ['cco', 'application/x-cocoa'],
+    ['ccxml', 'application/ccxml+xml,'],
+    ['cdbcmsg', 'application/vnd.contact.cmsg'],
+    ['cdf', ['application/cdf',
+        'application/x-cdf',
+        'application/x-netcdf'
+    ]],
+    ['cdkey', 'application/vnd.mediastation.cdkey'],
+    ['cdmia', 'application/cdmi-capability'],
+    ['cdmic', 'application/cdmi-container'],
+    ['cdmid', 'application/cdmi-domain'],
+    ['cdmio', 'application/cdmi-object'],
+    ['cdmiq', 'application/cdmi-queue'],
+    ['cdx', 'chemical/x-cdx'],
+    ['cdxml', 'application/vnd.chemdraw+xml'],
+    ['cdy', 'application/vnd.cinderella'],
+    ['cer', ['application/pkix-cert', 'application/x-x509-ca-cert']],
+    ['cgm', 'image/cgm'],
+    ['cha', 'application/x-chat'],
+    ['chat', 'application/x-chat'],
+    ['chm', 'application/vnd.ms-htmlhelp'],
+    ['chrt', 'application/vnd.kde.kchart'],
+    ['cif', 'chemical/x-cif'],
+    ['cii',
+        'application/vnd.anser-web-certificate-issue-initiation'
+    ],
+    ['cil', 'application/vnd.ms-artgalry'],
+    ['cla', 'application/vnd.claymore'],
+    ['class', ['application/octet-stream',
+        'application/java',
+        'application/java-byte-code',
+        'application/java-vm',
+        'application/x-java-class'
+    ]],
+    ['clkk', 'application/vnd.crick.clicker.keyboard'],
+    ['clkp', 'application/vnd.crick.clicker.palette'],
+    ['clkt', 'application/vnd.crick.clicker.template'],
+    ['clkw', 'application/vnd.crick.clicker.wordbank'],
+    ['clkx', 'application/vnd.crick.clicker'],
+    ['clp', 'application/x-msclip'],
+    ['cmc', 'application/vnd.cosmocaller'],
+    ['cmdf', 'chemical/x-cmdf'],
+    ['cml', 'chemical/x-cml'],
+    ['cmp', 'application/vnd.yellowriver-custom-menu'],
+    ['cmx', 'image/x-cmx'],
+    ['cod', ['image/cis-cod', 'application/vnd.rim.cod']],
+    ['com', ['application/octet-stream', 'text/plain']],
+    ['conf', 'text/plain'],
+    ['cpio', 'application/x-cpio'],
+    ['cpp', 'text/x-c'],
+    ['cpt', ['application/mac-compactpro',
+        'application/x-compactpro',
+        'application/x-cpt'
+    ]],
+    ['crd', 'application/x-mscardfile'],
+    ['crl', ['application/pkix-crl', 'application/pkcs-crl']],
+    ['crt', ['application/pkix-cert',
+        'application/x-x509-user-cert',
+        'application/x-x509-ca-cert'
+    ]],
+    ['cryptonote', 'application/vnd.rig.cryptonote'],
+    ['csh', ['text/x-script.csh', 'application/x-csh']],
+    ['csml', 'chemical/x-csml'],
+    ['csp', 'application/vnd.commonspace'],
+    ['css', ['text/css', 'application/x-pointplus']],
+    ['csv', 'text/csv'],
+    ['cu', 'application/cu-seeme'],
+    ['curl', 'text/vnd.curl'],
+    ['cww', 'application/prs.cww'],
+    ['cxx', 'text/plain'],
+    ['dae', 'model/vnd.collada+xml'],
+    ['daf', 'application/vnd.mobius.daf'],
+    ['davmount', 'application/davmount+xml'],
+    ['dcr', 'application/x-director'],
+    ['dcurl', 'text/vnd.curl.dcurl'],
+    ['dd2', 'application/vnd.oma.dd2+xml'],
+    ['ddd', 'application/vnd.fujixerox.ddd'],
+    ['deb', 'application/x-debian-package'],
+    ['deepv', 'application/x-deepv'],
+    ['def', 'text/plain'],
+    ['der', 'application/x-x509-ca-cert'],
+    ['dfac', 'application/vnd.dreamfactory'],
+    ['dif', 'video/x-dv'],
+    ['dir', 'application/x-director'],
+    ['dis', 'application/vnd.mobius.dis'],
+    ['djvu', 'image/vnd.djvu'],
+    ['dl', ['video/dl', 'video/x-dl']],
+    ['dll', 'application/x-msdownload'],
+    ['dms', 'application/octet-stream'],
+    ['dna', 'application/vnd.dna'],
+    ['doc', 'application/msword'],
+    ['docm', 'application/vnd.ms-word.document.macroenabled.12'],
+    ['docx',
+        'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
+    ],
+    ['dot', 'application/msword'],
+    ['dotm', 'application/vnd.ms-word.template.macroenabled.12'],
+    ['dotx',
+        'application/vnd.openxmlformats-officedocument.wordprocessingml.template'
+    ],
+    ['dp', ['application/commonground', 'application/vnd.osgi.dp']],
+    ['dpg', 'application/vnd.dpgraph'],
+    ['dra', 'audio/vnd.dra'],
+    ['drw', 'application/drafting'],
+    ['dsc', 'text/prs.lines.tag'],
+    ['dssc', 'application/dssc+der'],
+    ['dtb', 'application/x-dtbook+xml'],
+    ['dtd', 'application/xml-dtd'],
+    ['dts', 'audio/vnd.dts'],
+    ['dtshd', 'audio/vnd.dts.hd'],
+    ['dump', 'application/octet-stream'],
+    ['dv', 'video/x-dv'],
+    ['dvi', 'application/x-dvi'],
+    ['dwf', ['model/vnd.dwf', 'drawing/x-dwf']],
+    ['dwg', ['application/acad', 'image/vnd.dwg', 'image/x-dwg']],
+    ['dxf', ['application/dxf',
+        'image/vnd.dwg',
+        'image/vnd.dxf',
+        'image/x-dwg'
+    ]],
+    ['dxp', 'application/vnd.spotfire.dxp'],
+    ['dxr', 'application/x-director'],
+    ['ecelp4800', 'audio/vnd.nuera.ecelp4800'],
+    ['ecelp7470', 'audio/vnd.nuera.ecelp7470'],
+    ['ecelp9600', 'audio/vnd.nuera.ecelp9600'],
+    ['edm', 'application/vnd.novadigm.edm'],
+    ['edx', 'application/vnd.novadigm.edx'],
+    ['efif', 'application/vnd.picsel'],
+    ['ei6', 'application/vnd.pg.osasli'],
+    ['el', 'text/x-script.elisp'],
+    ['elc', ['application/x-elc', 'application/x-bytecode.elisp']],
+    ['eml', 'message/rfc822'],
+    ['emma', 'application/emma+xml'],
+    ['env', 'application/x-envoy'],
+    ['eol', 'audio/vnd.digital-winds'],
+    ['eot', 'application/vnd.ms-fontobject'],
+    ['eps', 'application/postscript'],
+    ['epub', 'application/epub+zip'],
+    ['es', ['application/ecmascript', 'application/x-esrehber']],
+    ['es3', 'application/vnd.eszigno3+xml'],
+    ['esf', 'application/vnd.epson.esf'],
+    ['etx', 'text/x-setext'],
+    ['evy', ['application/envoy', 'application/x-envoy']],
+    ['exe', ['application/octet-stream', 'application/x-msdownload']],
+    ['exi', 'application/exi'],
+    ['ext', 'application/vnd.novadigm.ext'],
+    ['ez2', 'application/vnd.ezpix-album'],
+    ['ez3', 'application/vnd.ezpix-package'],
+    ['f', ['text/plain', 'text/x-fortran']],
+    ['f4v', 'video/x-f4v'],
+    ['f77', 'text/x-fortran'],
+    ['f90', ['text/plain', 'text/x-fortran']],
+    ['fbs', 'image/vnd.fastbidsheet'],
+    ['fcs', 'application/vnd.isac.fcs'],
+    ['fdf', 'application/vnd.fdf'],
+    ['fe_launch', 'application/vnd.denovo.fcselayout-link'],
+    ['fg5', 'application/vnd.fujitsu.oasysgp'],
+    ['fh', 'image/x-freehand'],
+    ['fif', ['application/fractals', 'image/fif']],
+    ['fig', 'application/x-xfig'],
+    ['fli', ['video/fli', 'video/x-fli']],
+    ['flo', ['image/florian', 'application/vnd.micrografx.flo']],
+    ['flr', 'x-world/x-vrml'],
+    ['flv', 'video/x-flv'],
+    ['flw', 'application/vnd.kde.kivio'],
+    ['flx', 'text/vnd.fmi.flexstor'],
+    ['fly', 'text/vnd.fly'],
+    ['fm', 'application/vnd.framemaker'],
+    ['fmf', 'video/x-atomic3d-feature'],
+    ['fnc', 'application/vnd.frogans.fnc'],
+    ['for', ['text/plain', 'text/x-fortran']],
+    ['fpx', ['image/vnd.fpx', 'image/vnd.net-fpx']],
+    ['frl', 'application/freeloader'],
+    ['fsc', 'application/vnd.fsc.weblaunch'],
+    ['fst', 'image/vnd.fst'],
+    ['ftc', 'application/vnd.fluxtime.clip'],
+    ['fti', 'application/vnd.anser-web-funds-transfer-initiation'],
+    ['funk', 'audio/make'],
+    ['fvt', 'video/vnd.fvt'],
+    ['fxp', 'application/vnd.adobe.fxp'],
+    ['fzs', 'application/vnd.fuzzysheet'],
+    ['g', 'text/plain'],
+    ['g2w', 'application/vnd.geoplan'],
+    ['g3', 'image/g3fax'],
+    ['g3w', 'application/vnd.geospace'],
+    ['gac', 'application/vnd.groove-account'],
+    ['gdl', 'model/vnd.gdl'],
+    ['geo', 'application/vnd.dynageo'],
+    ['gex', 'application/vnd.geometry-explorer'],
+    ['ggb', 'application/vnd.geogebra.file'],
+    ['ggt', 'application/vnd.geogebra.tool'],
+    ['ghf', 'application/vnd.groove-help'],
+    ['gif', 'image/gif'],
+    ['gim', 'application/vnd.groove-identity-message'],
+    ['gl', ['video/gl', 'video/x-gl']],
+    ['gmx', 'application/vnd.gmx'],
+    ['gnumeric', 'application/x-gnumeric'],
+    ['gph', 'application/vnd.flographit'],
+    ['gqf', 'application/vnd.grafeq'],
+    ['gram', 'application/srgs'],
+    ['grv', 'application/vnd.groove-injector'],
+    ['grxml', 'application/srgs+xml'],
+    ['gsd', 'audio/x-gsm'],
+    ['gsf', 'application/x-font-ghostscript'],
+    ['gsm', 'audio/x-gsm'],
+    ['gsp', 'application/x-gsp'],
+    ['gss', 'application/x-gss'],
+    ['gtar', 'application/x-gtar'],
+    ['gtm', 'application/vnd.groove-tool-message'],
+    ['gtw', 'model/vnd.gtw'],
+    ['gv', 'text/vnd.graphviz'],
+    ['gxt', 'application/vnd.geonext'],
+    ['gz', ['application/x-gzip', 'application/x-compressed']],
+    ['gzip', ['multipart/x-gzip', 'application/x-gzip']],
+    ['h', ['text/plain', 'text/x-h']],
+    ['h261', 'video/h261'],
+    ['h263', 'video/h263'],
+    ['h264', 'video/h264'],
+    ['hal', 'application/vnd.hal+xml'],
+    ['hbci', 'application/vnd.hbci'],
+    ['hdf', 'application/x-hdf'],
+    ['help', 'application/x-helpfile'],
+    ['hgl', 'application/vnd.hp-hpgl'],
+    ['hh', ['text/plain', 'text/x-h']],
+    ['hlb', 'text/x-script'],
+    ['hlp', ['application/winhlp',
+        'application/hlp',
+        'application/x-helpfile',
+        'application/x-winhelp'
+    ]],
+    ['hpg', 'application/vnd.hp-hpgl'],
+    ['hpgl', 'application/vnd.hp-hpgl'],
+    ['hpid', 'application/vnd.hp-hpid'],
+    ['hps', 'application/vnd.hp-hps'],
+    ['hqx', ['application/mac-binhex40',
+        'application/binhex',
+        'application/binhex4',
+        'application/mac-binhex',
+        'application/x-binhex40',
+        'application/x-mac-binhex40'
+    ]],
+    ['hta', 'application/hta'],
+    ['htc', 'text/x-component'],
+    ['htke', 'application/vnd.kenameaapp'],
+    ['htm', 'text/html'],
+    ['html', 'text/html'],
+    ['htmls', 'text/html'],
+    ['htt', 'text/webviewhtml'],
+    ['htx', 'text/html'],
+    ['hvd', 'application/vnd.yamaha.hv-dic'],
+    ['hvp', 'application/vnd.yamaha.hv-voice'],
+    ['hvs', 'application/vnd.yamaha.hv-script'],
+    ['i2g', 'application/vnd.intergeo'],
+    ['icc', 'application/vnd.iccprofile'],
+    ['ice', 'x-conference/x-cooltalk'],
+    ['ico', 'image/x-icon'],
+    ['ics', 'text/calendar'],
+    ['idc', 'text/plain'],
+    ['ief', 'image/ief'],
+    ['iefs', 'image/ief'],
+    ['ifm', 'application/vnd.shana.informed.formdata'],
+    ['iges', ['application/iges', 'model/iges']],
+    ['igl', 'application/vnd.igloader'],
+    ['igm', 'application/vnd.insors.igm'],
+    ['igs', ['application/iges', 'model/iges']],
+    ['igx', 'application/vnd.micrografx.igx'],
+    ['iif', 'application/vnd.shana.informed.interchange'],
+    ['iii', 'application/x-iphone'],
+    ['ima', 'application/x-ima'],
+    ['imap', 'application/x-httpd-imap'],
+    ['imp', 'application/vnd.accpac.simply.imp'],
+    ['ims', 'application/vnd.ms-ims'],
+    ['inf', 'application/inf'],
+    ['ins', ['application/x-internet-signup',
+        'application/x-internett-signup'
+    ]],
+    ['ip', 'application/x-ip2'],
+    ['ipfix', 'application/ipfix'],
+    ['ipk', 'application/vnd.shana.informed.package'],
+    ['irm', 'application/vnd.ibm.rights-management'],
+    ['irp', 'application/vnd.irepository.package+xml'],
+    ['isp', 'application/x-internet-signup'],
+    ['isu', 'video/x-isvideo'],
+    ['it', 'audio/it'],
+    ['itp', 'application/vnd.shana.informed.formtemplate'],
+    ['iv', 'application/x-inventor'],
+    ['ivp', 'application/vnd.immervision-ivp'],
+    ['ivr', 'i-world/i-vrml'],
+    ['ivu', 'application/vnd.immervision-ivu'],
+    ['ivy', 'application/x-livescreen'],
+    ['jad', 'text/vnd.sun.j2me.app-descriptor'],
+    ['jam', ['application/vnd.jam', 'audio/x-jam']],
+    ['jar', 'application/java-archive'],
+    ['jav', ['text/plain', 'text/x-java-source']],
+    ['java', ['text/plain', 'text/x-java-source,java', 'text/x-java-source']],
+    ['jcm', 'application/x-java-commerce'],
+    ['jfif', ['image/pipeg', 'image/jpeg', 'image/pjpeg']],
+    ['jfif-tbnl', 'image/jpeg'],
+    ['jisp', 'application/vnd.jisp'],
+    ['jlt', 'application/vnd.hp-jlyt'],
+    ['jnlp', 'application/x-java-jnlp-file'],
+    ['joda', 'application/vnd.joost.joda-archive'],
+    ['jpe', ['image/jpeg', 'image/pjpeg']],
+    ['jpeg', ['image/jpeg', 'image/pjpeg']],
+    ['jpg', ['image/jpeg', 'image/pjpeg']],
+    ['jpgv', 'video/jpeg'],
+    ['jpm', 'video/jpm'],
+    ['jps', 'image/x-jps'],
+    ['js', ['application/javascript',
+        'application/ecmascript',
+        'text/javascript',
+        'text/ecmascript',
+        'application/x-javascript'
+    ]],
+    ['json', 'application/json'],
+    ['jut', 'image/jutvision'],
+    ['kar', ['audio/midi', 'music/x-karaoke']],
+    ['karbon', 'application/vnd.kde.karbon'],
+    ['kfo', 'application/vnd.kde.kformula'],
+    ['kia', 'application/vnd.kidspiration'],
+    ['kml', 'application/vnd.google-earth.kml+xml'],
+    ['kmz', 'application/vnd.google-earth.kmz'],
+    ['kne', 'application/vnd.kinar'],
+    ['kon', 'application/vnd.kde.kontour'],
+    ['kpr', 'application/vnd.kde.kpresenter'],
+    ['ksh', ['application/x-ksh', 'text/x-script.ksh']],
+    ['ksp', 'application/vnd.kde.kspread'],
+    ['ktx', 'image/ktx'],
+    ['ktz', 'application/vnd.kahootz'],
+    ['kwd', 'application/vnd.kde.kword'],
+    ['la', ['audio/nspaudio', 'audio/x-nspaudio']],
+    ['lam', 'audio/x-liveaudio'],
+    ['lasxml', 'application/vnd.las.las+xml'],
+    ['latex', 'application/x-latex'],
+    ['lbd', 'application/vnd.llamagraphics.life-balance.desktop'],
+    ['lbe',
+        'application/vnd.llamagraphics.life-balance.exchange+xml'
+    ],
+    ['les', 'application/vnd.hhe.lesson-player'],
+    ['lha', ['application/octet-stream',
+        'application/lha',
+        'application/x-lha'
+    ]],
+    ['lhx', 'application/octet-stream'],
+    ['link66', 'application/vnd.route66.link66+xml'],
+    ['list', 'text/plain'],
+    ['lma', ['audio/nspaudio', 'audio/x-nspaudio']],
+    ['log', 'text/plain'],
+    ['lrm', 'application/vnd.ms-lrm'],
+    ['lsf', 'video/x-la-asf'],
+    ['lsp', ['application/x-lisp', 'text/x-script.lisp']],
+    ['lst', 'text/plain'],
+    ['lsx', ['video/x-la-asf', 'text/x-la-asf']],
+    ['ltf', 'application/vnd.frogans.ltf'],
+    ['ltx', 'application/x-latex'],
+    ['lvp', 'audio/vnd.lucent.voice'],
+    ['lwp', 'application/vnd.lotus-wordpro'],
+    ['lzh', ['application/octet-stream', 'application/x-lzh']],
+    ['lzx', ['application/lzx',
+        'application/octet-stream',
+        'application/x-lzx'
+    ]],
+    ['m', ['text/plain', 'text/x-m']],
+    ['m13', 'application/x-msmediaview'],
+    ['m14', 'application/x-msmediaview'],
+    ['m1v', 'video/mpeg'],
+    ['m21', 'application/mp21'],
+    ['m2a', 'audio/mpeg'],
+    ['m2v', 'video/mpeg'],
+    ['m3u', ['audio/x-mpegurl', 'audio/x-mpequrl']],
+    ['m3u8', 'application/vnd.apple.mpegurl'],
+    ['m4v', 'video/x-m4v'],
+    ['ma', 'application/mathematica'],
+    ['mads', 'application/mads+xml'],
+    ['mag', 'application/vnd.ecowin.chart'],
+    ['man', 'application/x-troff-man'],
+    ['map', 'application/x-navimap'],
+    ['mar', 'text/plain'],
+    ['mathml', 'application/mathml+xml'],
+    ['mbd', 'application/mbedlet'],
+    ['mbk', 'application/vnd.mobius.mbk'],
+    ['mbox', 'application/mbox'],
+    ['mc$', 'application/x-magic-cap-package-1.0'],
+    ['mc1', 'application/vnd.medcalcdata'],
+    ['mcd', ['application/mcad',
+        'application/vnd.mcd',
+        'application/x-mathcad'
+    ]],
+    ['mcf', ['image/vasa', 'text/mcf']],
+    ['mcp', 'application/netmc'],
+    ['mcurl', 'text/vnd.curl.mcurl'],
+    ['mdb', 'application/x-msaccess'],
+    ['mdi', 'image/vnd.ms-modi'],
+    ['me', 'application/x-troff-me'],
+    ['meta4', 'application/metalink4+xml'],
+    ['mets', 'application/mets+xml'],
+    ['mfm', 'application/vnd.mfmp'],
+    ['mgp', 'application/vnd.osgeo.mapguide.package'],
+    ['mgz', 'application/vnd.proteus.magazine'],
+    ['mht', 'message/rfc822'],
+    ['mhtml', 'message/rfc822'],
+    ['mid', ['audio/mid',
+        'audio/midi',
+        'music/crescendo',
+        'x-music/x-midi',
+        'audio/x-midi',
+        'application/x-midi',
+        'audio/x-mid'
+    ]],
+    ['midi', ['audio/midi',
+        'music/crescendo',
+        'x-music/x-midi',
+        'audio/x-midi',
+        'application/x-midi',
+        'audio/x-mid'
+    ]],
+    ['mif', ['application/vnd.mif',
+        'application/x-mif',
+        'application/x-frame'
+    ]],
+    ['mime', ['message/rfc822', 'www/mime']],
+    ['mj2', 'video/mj2'],
+    ['mjf', 'audio/x-vnd.audioexplosion.mjuicemediafile'],
+    ['mjpg', 'video/x-motion-jpeg'],
+    ['mlp', 'application/vnd.dolby.mlp'],
+    ['mm', ['application/base64', 'application/x-meme']],
+    ['mmd', 'application/vnd.chipnuts.karaoke-mmd'],
+    ['mme', 'application/base64'],
+    ['mmf', 'application/vnd.smaf'],
+    ['mmr', 'image/vnd.fujixerox.edmics-mmr'],
+    ['mny', 'application/x-msmoney'],
+    ['mod', ['audio/mod', 'audio/x-mod']],
+    ['mods', 'application/mods+xml'],
+    ['moov', 'video/quicktime'],
+    ['mov', 'video/quicktime'],
+    ['movie', 'video/x-sgi-movie'],
+    ['mp2', ['video/mpeg',
+        'audio/mpeg',
+        'video/x-mpeg',
+        'audio/x-mpeg',
+        'video/x-mpeq2a'
+    ]],
+    ['mp3', ['audio/mpeg',
+        'audio/mpeg3',
+        'video/mpeg',
+        'audio/x-mpeg-3',
+        'video/x-mpeg'
+    ]],
+    ['mp4', ['video/mp4', 'application/mp4']],
+    ['mp4a', 'audio/mp4'],
+    ['mpa', ['video/mpeg', 'audio/mpeg']],
+    ['mpc', ['application/vnd.mophun.certificate',
+        'application/x-project'
+    ]],
+    ['mpe', 'video/mpeg'],
+    ['mpeg', 'video/mpeg'],
+    ['mpg', ['video/mpeg', 'audio/mpeg']],
+    ['mpga', 'audio/mpeg'],
+    ['mpkg', 'application/vnd.apple.installer+xml'],
+    ['mpm', 'application/vnd.blueice.multipass'],
+    ['mpn', 'application/vnd.mophun.application'],
+    ['mpp', 'application/vnd.ms-project'],
+    ['mpt', 'application/x-project'],
+    ['mpv', 'application/x-project'],
+    ['mpv2', 'video/mpeg'],
+    ['mpx', 'application/x-project'],
+    ['mpy', 'application/vnd.ibm.minipay'],
+    ['mqy', 'application/vnd.mobius.mqy'],
+    ['mrc', 'application/marc'],
+    ['mrcx', 'application/marcxml+xml'],
+    ['ms', 'application/x-troff-ms'],
+    ['mscml', 'application/mediaservercontrol+xml'],
+    ['mseq', 'application/vnd.mseq'],
+    ['msf', 'application/vnd.epson.msf'],
+    ['msg', 'application/vnd.ms-outlook'],
+    ['msh', 'model/mesh'],
+    ['msl', 'application/vnd.mobius.msl'],
+    ['msty', 'application/vnd.muvee.style'],
+    ['mts', 'model/vnd.mts'],
+    ['mus', 'application/vnd.musician'],
+    ['musicxml', 'application/vnd.recordare.musicxml+xml'],
+    ['mv', 'video/x-sgi-movie'],
+    ['mvb', 'application/x-msmediaview'],
+    ['mwf', 'application/vnd.mfer'],
+    ['mxf', 'application/mxf'],
+    ['mxl', 'application/vnd.recordare.musicxml'],
+    ['mxml', 'application/xv+xml'],
+    ['mxs', 'application/vnd.triscape.mxs'],
+    ['mxu', 'video/vnd.mpegurl'],
+    ['my', 'audio/make'],
+    ['mzz', 'application/x-vnd.audioexplosion.mzz'],
+    ['n-gage', 'application/vnd.nokia.n-gage.symbian.install'],
+    ['n3', 'text/n3'],
+    ['nap', 'image/naplps'],
+    ['naplps', 'image/naplps'],
+    ['nbp', 'application/vnd.wolfram.player'],
+    ['nc', 'application/x-netcdf'],
+    ['ncm', 'application/vnd.nokia.configuration-message'],
+    ['ncx', 'application/x-dtbncx+xml'],
+    ['ngdat', 'application/vnd.nokia.n-gage.data'],
+    ['nif', 'image/x-niff'],
+    ['niff', 'image/x-niff'],
+    ['nix', 'application/x-mix-transfer'],
+    ['nlu', 'application/vnd.neurolanguage.nlu'],
+    ['nml', 'application/vnd.enliven'],
+    ['nnd', 'application/vnd.noblenet-directory'],
+    ['nns', 'application/vnd.noblenet-sealer'],
+    ['nnw', 'application/vnd.noblenet-web'],
+    ['npx', 'image/vnd.net-fpx'],
+    ['nsc', 'application/x-conference'],
+    ['nsf', 'application/vnd.lotus-notes'],
+    ['nvd', 'application/x-navidoc'],
+    ['nws', 'message/rfc822'],
+    ['o', 'application/octet-stream'],
+    ['oa2', 'application/vnd.fujitsu.oasys2'],
+    ['oa3', 'application/vnd.fujitsu.oasys3'],
+    ['oas', 'application/vnd.fujitsu.oasys'],
+    ['obd', 'application/x-msbinder'],
+    ['oda', 'application/oda'],
+    ['odb', 'application/vnd.oasis.opendocument.database'],
+    ['odc', 'application/vnd.oasis.opendocument.chart'],
+    ['odf', 'application/vnd.oasis.opendocument.formula'],
+    ['odft',
+        'application/vnd.oasis.opendocument.formula-template'
+    ],
+    ['odg', 'application/vnd.oasis.opendocument.graphics'],
+    ['odi', 'application/vnd.oasis.opendocument.image'],
+    ['odm', 'application/vnd.oasis.opendocument.text-master'],
+    ['odp', 'application/vnd.oasis.opendocument.presentation'],
+    ['ods', 'application/vnd.oasis.opendocument.spreadsheet'],
+    ['odt', 'application/vnd.oasis.opendocument.text'],
+    ['oga', 'audio/ogg'],
+    ['ogv', 'video/ogg'],
+    ['ogx', 'application/ogg'],
+    ['omc', 'application/x-omc'],
+    ['omcd', 'application/x-omcdatamaker'],
+    ['omcr', 'application/x-omcregerator'],
+    ['onetoc', 'application/onenote'],
+    ['opf', 'application/oebps-package+xml'],
+    ['org', 'application/vnd.lotus-organizer'],
+    ['osf', 'application/vnd.yamaha.openscoreformat'],
+    ['osfpvg',
+        'application/vnd.yamaha.openscoreformat.osfpvg+xml'
+    ],
+    ['otc', 'application/vnd.oasis.opendocument.chart-template'],
+    ['otf', 'application/x-font-otf'],
+    ['otg',
+        'application/vnd.oasis.opendocument.graphics-template'
+    ],
+    ['oth', 'application/vnd.oasis.opendocument.text-web'],
+    ['oti', 'application/vnd.oasis.opendocument.image-template'],
+    ['otp',
+        'application/vnd.oasis.opendocument.presentation-template'
+    ],
+    ['ots',
+        'application/vnd.oasis.opendocument.spreadsheet-template'
+    ],
+    ['ott', 'application/vnd.oasis.opendocument.text-template'],
+    ['oxt', 'application/vnd.openofficeorg.extension'],
+    ['p', 'text/x-pascal'],
+    ['p10', ['application/pkcs10', 'application/x-pkcs10']],
+    ['p12', ['application/pkcs-12', 'application/x-pkcs12']],
+    ['p7a', 'application/x-pkcs7-signature'],
+    ['p7b', 'application/x-pkcs7-certificates'],
+    ['p7c', ['application/pkcs7-mime', 'application/x-pkcs7-mime']],
+    ['p7m', ['application/pkcs7-mime', 'application/x-pkcs7-mime']],
+    ['p7r', 'application/x-pkcs7-certreqresp'],
+    ['p7s', ['application/pkcs7-signature',
+        'application/x-pkcs7-signature'
+    ]],
+    ['p8', 'application/pkcs8'],
+    ['par', 'text/plain-bas'],
+    ['part', 'application/pro_eng'],
+    ['pas', 'text/pascal'],
+    ['paw', 'application/vnd.pawaafile'],
+    ['pbd', 'application/vnd.powerbuilder6'],
+    ['pbm', 'image/x-portable-bitmap'],
+    ['pcf', 'application/x-font-pcf'],
+    ['pcl', ['application/vnd.hp-pcl', 'application/x-pcl']],
+    ['pclxl', 'application/vnd.hp-pclxl'],
+    ['pct', 'image/x-pict'],
+    ['pcurl', 'application/vnd.curl.pcurl'],
+    ['pcx', 'image/x-pcx'],
+    ['pdb', ['application/vnd.palm', 'chemical/x-pdb']],
+    ['pdf', 'application/pdf'],
+    ['pfa', 'application/x-font-type1'],
+    ['pfr', 'application/font-tdpfr'],
+    ['pfunk', ['audio/make', 'audio/make.my.funk']],
+    ['pfx', 'application/x-pkcs12'],
+    ['pgm', ['image/x-portable-graymap', 'image/x-portable-greymap']],
+    ['pgn', 'application/x-chess-pgn'],
+    ['pgp', 'application/pgp-signature'],
+    ['pic', ['image/pict', 'image/x-pict']],
+    ['pict', 'image/pict'],
+    ['pkg', 'application/x-newton-compatible-pkg'],
+    ['pki', 'application/pkixcmp'],
+    ['pkipath', 'application/pkix-pkipath'],
+    ['pko', ['application/ynd.ms-pkipko', 'application/vnd.ms-pki.pko']],
+    ['pl', ['text/plain', 'text/x-script.perl']],
+    ['plb', 'application/vnd.3gpp.pic-bw-large'],
+    ['plc', 'application/vnd.mobius.plc'],
+    ['plf', 'application/vnd.pocketlearn'],
+    ['pls', 'application/pls+xml'],
+    ['plx', 'application/x-pixclscript'],
+    ['pm', ['text/x-script.perl-module', 'image/x-xpixmap']],
+    ['pm4', 'application/x-pagemaker'],
+    ['pm5', 'application/x-pagemaker'],
+    ['pma', 'application/x-perfmon'],
+    ['pmc', 'application/x-perfmon'],
+    ['pml', ['application/vnd.ctc-posml', 'application/x-perfmon']],
+    ['pmr', 'application/x-perfmon'],
+    ['pmw', 'application/x-perfmon'],
+    ['png', 'image/png'],
+    ['pnm', ['application/x-portable-anymap', 'image/x-portable-anymap']],
+    ['portpkg', 'application/vnd.macports.portpkg'],
+    ['pot', ['application/vnd.ms-powerpoint', 'application/mspowerpoint']],
+    ['potm',
+        'application/vnd.ms-powerpoint.template.macroenabled.12'
+    ],
+    ['potx',
+        'application/vnd.openxmlformats-officedocument.presentationml.template'
+    ],
+    ['pov', 'model/x-pov'],
+    ['ppa', 'application/vnd.ms-powerpoint'],
+    ['ppam',
+        'application/vnd.ms-powerpoint.addin.macroenabled.12'
+    ],
+    ['ppd', 'application/vnd.cups-ppd'],
+    ['ppm', 'image/x-portable-pixmap'],
+    ['pps', ['application/vnd.ms-powerpoint', 'application/mspowerpoint']],
+    ['ppsm',
+        'application/vnd.ms-powerpoint.slideshow.macroenabled.12'
+    ],
+    ['ppsx',
+        'application/vnd.openxmlformats-officedocument.presentationml.slideshow'
+    ],
+    ['ppt', ['application/vnd.ms-powerpoint',
+        'application/mspowerpoint',
+        'application/powerpoint',
+        'application/x-mspowerpoint'
+    ]],
+    ['pptm',
+        'application/vnd.ms-powerpoint.presentation.macroenabled.12'
+    ],
+    ['pptx',
+        'application/vnd.openxmlformats-officedocument.presentationml.presentation'
+    ],
+    ['ppz', 'application/mspowerpoint'],
+    ['prc', 'application/x-mobipocket-ebook'],
+    ['pre', ['application/vnd.lotus-freelance', 'application/x-freelance']],
+    ['prf', 'application/pics-rules'],
+    ['prt', 'application/pro_eng'],
+    ['ps', 'application/postscript'],
+    ['psb', 'application/vnd.3gpp.pic-bw-small'],
+    ['psd', ['application/octet-stream', 'image/vnd.adobe.photoshop']],
+    ['psf', 'application/x-font-linux-psf'],
+    ['pskcxml', 'application/pskc+xml'],
+    ['ptid', 'application/vnd.pvi.ptid1'],
+    ['pub', 'application/x-mspublisher'],
+    ['pvb', 'application/vnd.3gpp.pic-bw-var'],
+    ['pvu', 'paleovu/x-pv'],
+    ['pwn', 'application/vnd.3m.post-it-notes'],
+    ['pwz', 'application/vnd.ms-powerpoint'],
+    ['py', 'text/x-script.phyton'],
+    ['pya', 'audio/vnd.ms-playready.media.pya'],
+    ['pyc', 'applicaiton/x-bytecode.python'],
+    ['pyv', 'video/vnd.ms-playready.media.pyv'],
+    ['qam', 'application/vnd.epson.quickanime'],
+    ['qbo', 'application/vnd.intu.qbo'],
+    ['qcp', 'audio/vnd.qcelp'],
+    ['qd3', 'x-world/x-3dmf'],
+    ['qd3d', 'x-world/x-3dmf'],
+    ['qfx', 'application/vnd.intu.qfx'],
+    ['qif', 'image/x-quicktime'],
+    ['qps', 'application/vnd.publishare-delta-tree'],
+    ['qt', 'video/quicktime'],
+    ['qtc', 'video/x-qtc'],
+    ['qti', 'image/x-quicktime'],
+    ['qtif', 'image/x-quicktime'],
+    ['qxd', 'application/vnd.quark.quarkxpress'],
+    ['ra', ['audio/x-realaudio',
+        'audio/x-pn-realaudio',
+        'audio/x-pn-realaudio-plugin'
+    ]],
+    ['ram', 'audio/x-pn-realaudio'],
+    ['rar', 'application/x-rar-compressed'],
+    ['ras', ['image/cmu-raster',
+        'application/x-cmu-raster',
+        'image/x-cmu-raster'
+    ]],
+    ['rast', 'image/cmu-raster'],
+    ['rcprofile', 'application/vnd.ipunplugged.rcprofile'],
+    ['rdf', 'application/rdf+xml'],
+    ['rdz', 'application/vnd.data-vision.rdz'],
+    ['rep', 'application/vnd.businessobjects'],
+    ['res', 'application/x-dtbresource+xml'],
+    ['rexx', 'text/x-script.rexx'],
+    ['rf', 'image/vnd.rn-realflash'],
+    ['rgb', 'image/x-rgb'],
+    ['rif', 'application/reginfo+xml'],
+    ['rip', 'audio/vnd.rip'],
+    ['rl', 'application/resource-lists+xml'],
+    ['rlc', 'image/vnd.fujixerox.edmics-rlc'],
+    ['rld', 'application/resource-lists-diff+xml'],
+    ['rm', ['application/vnd.rn-realmedia', 'audio/x-pn-realaudio']],
+    ['rmi', 'audio/mid'],
+    ['rmm', 'audio/x-pn-realaudio'],
+    ['rmp', ['audio/x-pn-realaudio-plugin', 'audio/x-pn-realaudio']],
+    ['rms', 'application/vnd.jcp.javame.midlet-rms'],
+    ['rnc', 'application/relax-ng-compact-syntax'],
+    ['rng', ['application/ringing-tones',
+        'application/vnd.nokia.ringing-tone'
+    ]],
+    ['rnx', 'application/vnd.rn-realplayer'],
+    ['roff', 'application/x-troff'],
+    ['rp', 'image/vnd.rn-realpix'],
+    ['rp9', 'application/vnd.cloanto.rp9'],
+    ['rpm', 'audio/x-pn-realaudio-plugin'],
+    ['rpss', 'application/vnd.nokia.radio-presets'],
+    ['rpst', 'application/vnd.nokia.radio-preset'],
+    ['rq', 'application/sparql-query'],
+    ['rs', 'application/rls-services+xml'],
+    ['rsd', 'application/rsd+xml'],
+    ['rt', ['text/richtext', 'text/vnd.rn-realtext']],
+    ['rtf', ['application/rtf', 'text/richtext', 'application/x-rtf']],
+    ['rtx', ['text/richtext', 'application/rtf']],
+    ['rv', 'video/vnd.rn-realvideo'],
+    ['s', 'text/x-asm'],
+    ['s3m', 'audio/s3m'],
+    ['saf', 'application/vnd.yamaha.smaf-audio'],
+    ['saveme', 'application/octet-stream'],
+    ['sbk', 'application/x-tbook'],
+    ['sbml', 'application/sbml+xml'],
+    ['sc', 'application/vnd.ibm.secure-container'],
+    ['scd', 'application/x-msschedule'],
+    ['scm', ['application/vnd.lotus-screencam',
+        'video/x-scm',
+        'text/x-script.guile',
+        'application/x-lotusscreencam',
+        'text/x-script.scheme'
+    ]],
+    ['scq', 'application/scvp-cv-request'],
+    ['scs', 'application/scvp-cv-response'],
+    ['sct', 'text/scriptlet'],
+    ['scurl', 'text/vnd.curl.scurl'],
+    ['sda', 'application/vnd.stardivision.draw'],
+    ['sdc', 'application/vnd.stardivision.calc'],
+    ['sdd', 'application/vnd.stardivision.impress'],
+    ['sdkm', 'application/vnd.solent.sdkm+xml'],
+    ['sdml', 'text/plain'],
+    ['sdp', ['application/sdp', 'application/x-sdp']],
+    ['sdr', 'application/sounder'],
+    ['sdw', 'application/vnd.stardivision.writer'],
+    ['sea', ['application/sea', 'application/x-sea']],
+    ['see', 'application/vnd.seemail'],
+    ['seed', 'application/vnd.fdsn.seed'],
+    ['sema', 'application/vnd.sema'],
+    ['semd', 'application/vnd.semd'],
+    ['semf', 'application/vnd.semf'],
+    ['ser', 'application/java-serialized-object'],
+    ['set', 'application/set'],
+    ['setpay', 'application/set-payment-initiation'],
+    ['setreg', 'application/set-registration-initiation'],
+    ['sfd-hdstx', 'application/vnd.hydrostatix.sof-data'],
+    ['sfs', 'application/vnd.spotfire.sfs'],
+    ['sgl', 'application/vnd.stardivision.writer-global'],
+    ['sgm', ['text/sgml', 'text/x-sgml']],
+    ['sgml', ['text/sgml', 'text/x-sgml']],
+    ['sh', ['application/x-shar',
+        'application/x-bsh',
+        'application/x-sh',
+        'text/x-script.sh'
+    ]],
+    ['shar', ['application/x-bsh', 'application/x-shar']],
+    ['shf', 'application/shf+xml'],
+    ['shtml', ['text/html', 'text/x-server-parsed-html']],
+    ['sid', 'audio/x-psid'],
+    ['sis', 'application/vnd.symbian.install'],
+    ['sit', ['application/x-stuffit', 'application/x-sit']],
+    ['sitx', 'application/x-stuffitx'],
+    ['skd', 'application/x-koan'],
+    ['skm', 'application/x-koan'],
+    ['skp', ['application/vnd.koan', 'application/x-koan']],
+    ['skt', 'application/x-koan'],
+    ['sl', 'application/x-seelogo'],
+    ['sldm',
+        'application/vnd.ms-powerpoint.slide.macroenabled.12'
+    ],
+    ['sldx',
+        'application/vnd.openxmlformats-officedocument.presentationml.slide'
+    ],
+    ['slt', 'application/vnd.epson.salt'],
+    ['sm', 'application/vnd.stepmania.stepchart'],
+    ['smf', 'application/vnd.stardivision.math'],
+    ['smi', ['application/smil', 'application/smil+xml']],
+    ['smil', 'application/smil'],
+    ['snd', ['audio/basic', 'audio/x-adpcm']],
+    ['snf', 'application/x-font-snf'],
+    ['sol', 'application/solids'],
+    ['spc', ['text/x-speech', 'application/x-pkcs7-certificates']],
+    ['spf', 'application/vnd.yamaha.smaf-phrase'],
+    ['spl', ['application/futuresplash', 'application/x-futuresplash']],
+    ['spot', 'text/vnd.in3d.spot'],
+    ['spp', 'application/scvp-vp-response'],
+    ['spq', 'application/scvp-vp-request'],
+    ['spr', 'application/x-sprite'],
+    ['sprite', 'application/x-sprite'],
+    ['src', 'application/x-wais-source'],
+    ['sru', 'application/sru+xml'],
+    ['srx', 'application/sparql-results+xml'],
+    ['sse', 'application/vnd.kodak-descriptor'],
+    ['ssf', 'application/vnd.epson.ssf'],
+    ['ssi', 'text/x-server-parsed-html'],
+    ['ssm', 'application/streamingmedia'],
+    ['ssml', 'application/ssml+xml'],
+    ['sst', ['application/vnd.ms-pkicertstore',
+        'application/vnd.ms-pki.certstore'
+    ]],
+    ['st', 'application/vnd.sailingtracker.track'],
+    ['stc', 'application/vnd.sun.xml.calc.template'],
+    ['std', 'application/vnd.sun.xml.draw.template'],
+    ['step', 'application/step'],
+    ['stf', 'application/vnd.wt.stf'],
+    ['sti', 'application/vnd.sun.xml.impress.template'],
+    ['stk', 'application/hyperstudio'],
+    ['stl', ['application/vnd.ms-pkistl',
+        'application/sla',
+        'application/vnd.ms-pki.stl',
+        'application/x-navistyle'
+    ]],
+    ['stm', 'text/html'],
+    ['stp', 'application/step'],
+    ['str', 'application/vnd.pg.format'],
+    ['stw', 'application/vnd.sun.xml.writer.template'],
+    ['sub', 'image/vnd.dvb.subtitle'],
+    ['sus', 'application/vnd.sus-calendar'],
+    ['sv4cpio', 'application/x-sv4cpio'],
+    ['sv4crc', 'application/x-sv4crc'],
+    ['svc', 'application/vnd.dvb.service'],
+    ['svd', 'application/vnd.svd'],
+    ['svf', ['image/vnd.dwg', 'image/x-dwg']],
+    ['svg', 'image/svg+xml'],
+    ['svr', ['x-world/x-svr', 'application/x-world']],
+    ['swf', 'application/x-shockwave-flash'],
+    ['swi', 'application/vnd.aristanetworks.swi'],
+    ['sxc', 'application/vnd.sun.xml.calc'],
+    ['sxd', 'application/vnd.sun.xml.draw'],
+    ['sxg', 'application/vnd.sun.xml.writer.global'],
+    ['sxi', 'application/vnd.sun.xml.impress'],
+    ['sxm', 'application/vnd.sun.xml.math'],
+    ['sxw', 'application/vnd.sun.xml.writer'],
+    ['t', ['text/troff', 'application/x-troff']],
+    ['talk', 'text/x-speech'],
+    ['tao', 'application/vnd.tao.intent-module-archive'],
+    ['tar', 'application/x-tar'],
+    ['tbk', ['application/toolbook', 'application/x-tbook']],
+    ['tcap', 'application/vnd.3gpp2.tcap'],
+    ['tcl', ['text/x-script.tcl', 'application/x-tcl']],
+    ['tcsh', 'text/x-script.tcsh'],
+    ['teacher', 'application/vnd.smart.teacher'],
+    ['tei', 'application/tei+xml'],
+    ['tex', 'application/x-tex'],
+    ['texi', 'application/x-texinfo'],
+    ['texinfo', 'application/x-texinfo'],
+    ['text', ['application/plain', 'text/plain']],
+    ['tfi', 'application/thraud+xml'],
+    ['tfm', 'application/x-tex-tfm'],
+    ['tgz', ['application/gnutar', 'application/x-compressed']],
+    ['thmx', 'application/vnd.ms-officetheme'],
+    ['tif', ['image/tiff', 'image/x-tiff']],
+    ['tiff', ['image/tiff', 'image/x-tiff']],
+    ['tmo', 'application/vnd.tmobile-livetv'],
+    ['torrent', 'application/x-bittorrent'],
+    ['tpl', 'application/vnd.groove-tool-template'],
+    ['tpt', 'application/vnd.trid.tpt'],
+    ['tr', 'application/x-troff'],
+    ['tra', 'application/vnd.trueapp'],
+    ['trm', 'application/x-msterminal'],
+    ['tsd', 'application/timestamped-data'],
+    ['tsi', 'audio/tsp-audio'],
+    ['tsp', ['application/dsptype', 'audio/tsplayer']],
+    ['tsv', 'text/tab-separated-values'],
+    ['ttf', 'application/x-font-ttf'],
+    ['ttl', 'text/turtle'],
+    ['turbot', 'image/florian'],
+    ['twd', 'application/vnd.simtech-mindmapper'],
+    ['txd', 'application/vnd.genomatix.tuxedo'],
+    ['txf', 'application/vnd.mobius.txf'],
+    ['txt', 'text/plain'],
+    ['ufd', 'application/vnd.ufdl'],
+    ['uil', 'text/x-uil'],
+    ['uls', 'text/iuls'],
+    ['umj', 'application/vnd.umajin'],
+    ['uni', 'text/uri-list'],
+    ['unis', 'text/uri-list'],
+    ['unityweb', 'application/vnd.unity'],
+    ['unv', 'application/i-deas'],
+    ['uoml', 'application/vnd.uoml+xml'],
+    ['uri', 'text/uri-list'],
+    ['uris', 'text/uri-list'],
+    ['ustar', ['application/x-ustar', 'multipart/x-ustar']],
+    ['utz', 'application/vnd.uiq.theme'],
+    ['uu', ['application/octet-stream', 'text/x-uuencode']],
+    ['uue', 'text/x-uuencode'],
+    ['uva', 'audio/vnd.dece.audio'],
+    ['uvh', 'video/vnd.dece.hd'],
+    ['uvi', 'image/vnd.dece.graphic'],
+    ['uvm', 'video/vnd.dece.mobile'],
+    ['uvp', 'video/vnd.dece.pd'],
+    ['uvs', 'video/vnd.dece.sd'],
+    ['uvu', 'video/vnd.uvvu.mp4'],
+    ['uvv', 'video/vnd.dece.video'],
+    ['vcd', 'application/x-cdlink'],
+    ['vcf', 'text/x-vcard'],
+    ['vcg', 'application/vnd.groove-vcard'],
+    ['vcs', 'text/x-vcalendar'],
+    ['vcx', 'application/vnd.vcx'],
+    ['vda', 'application/vda'],
+    ['vdo', 'video/vdo'],
+    ['vew', 'application/groupwise'],
+    ['vis', 'application/vnd.visionary'],
+    ['viv', ['video/vivo', 'video/vnd.vivo']],
+    ['vivo', ['video/vivo', 'video/vnd.vivo']],
+    ['vmd', 'application/vocaltec-media-desc'],
+    ['vmf', 'application/vocaltec-media-file'],
+    ['voc', ['audio/voc', 'audio/x-voc']],
+    ['vos', 'video/vosaic'],
+    ['vox', 'audio/voxware'],
+    ['vqe', 'audio/x-twinvq-plugin'],
+    ['vqf', 'audio/x-twinvq'],
+    ['vql', 'audio/x-twinvq-plugin'],
+    ['vrml', ['model/vrml', 'x-world/x-vrml', 'application/x-vrml']],
+    ['vrt', 'x-world/x-vrt'],
+    ['vsd', ['application/vnd.visio', 'application/x-visio']],
+    ['vsf', 'application/vnd.vsf'],
+    ['vst', 'application/x-visio'],
+    ['vsw', 'application/x-visio'],
+    ['vtu', 'model/vnd.vtu'],
+    ['vxml', 'application/voicexml+xml'],
+    ['w60', 'application/wordperfect6.0'],
+    ['w61', 'application/wordperfect6.1'],
+    ['w6w', 'application/msword'],
+    ['wad', 'application/x-doom'],
+    ['wav', ['audio/wav', 'audio/x-wav']],
+    ['wax', 'audio/x-ms-wax'],
+    ['wb1', 'application/x-qpro'],
+    ['wbmp', 'image/vnd.wap.wbmp'],
+    ['wbs', 'application/vnd.criticaltools.wbs+xml'],
+    ['wbxml', 'application/vnd.wap.wbxml'],
+    ['wcm', 'application/vnd.ms-works'],
+    ['wdb', 'application/vnd.ms-works'],
+    ['web', 'application/vnd.xara'],
+    ['weba', 'audio/webm'],
+    ['webm', 'video/webm'],
+    ['webp', 'image/webp'],
+    ['wg', 'application/vnd.pmi.widget'],
+    ['wgt', 'application/widget'],
+    ['wiz', 'application/msword'],
+    ['wk1', 'application/x-123'],
+    ['wks', 'application/vnd.ms-works'],
+    ['wm', 'video/x-ms-wm'],
+    ['wma', 'audio/x-ms-wma'],
+    ['wmd', 'application/x-ms-wmd'],
+    ['wmf', ['windows/metafile', 'application/x-msmetafile']],
+    ['wml', 'text/vnd.wap.wml'],
+    ['wmlc', 'application/vnd.wap.wmlc'],
+    ['wmls', 'text/vnd.wap.wmlscript'],
+    ['wmlsc', 'application/vnd.wap.wmlscriptc'],
+    ['wmv', 'video/x-ms-wmv'],
+    ['wmx', 'video/x-ms-wmx'],
+    ['wmz', 'application/x-ms-wmz'],
+    ['woff', 'application/x-font-woff'],
+    ['word', 'application/msword'],
+    ['wp', 'application/wordperfect'],
+    ['wp5', ['application/wordperfect', 'application/wordperfect6.0']],
+    ['wp6', 'application/wordperfect'],
+    ['wpd', ['application/wordperfect',
+        'application/vnd.wordperfect',
+        'application/x-wpwin'
+    ]],
+    ['wpl', 'application/vnd.ms-wpl'],
+    ['wps', 'application/vnd.ms-works'],
+    ['wq1', 'application/x-lotus'],
+    ['wqd', 'application/vnd.wqd'],
+    ['wri', ['application/mswrite',
+        'application/x-wri',
+        'application/x-mswrite'
+    ]],
+    ['wrl', ['model/vrml', 'x-world/x-vrml', 'application/x-world']],
+    ['wrz', ['model/vrml', 'x-world/x-vrml']],
+    ['wsc', 'text/scriplet'],
+    ['wsdl', 'application/wsdl+xml'],
+    ['wspolicy', 'application/wspolicy+xml'],
+    ['wsrc', 'application/x-wais-source'],
+    ['wtb', 'application/vnd.webturbo'],
+    ['wtk', 'application/x-wintalk'],
+    ['wvx', 'video/x-ms-wvx'],
+    ['x-png', 'image/png'],
+    ['x3d', 'application/vnd.hzn-3d-crossword'],
+    ['xaf', 'x-world/x-vrml'],
+    ['xap', 'application/x-silverlight-app'],
+    ['xar', 'application/vnd.xara'],
+    ['xbap', 'application/x-ms-xbap'],
+    ['xbd', 'application/vnd.fujixerox.docuworks.binder'],
+    ['xbm', ['image/xbm', 'image/x-xbm', 'image/x-xbitmap']],
+    ['xdf', 'application/xcap-diff+xml'],
+    ['xdm', 'application/vnd.syncml.dm+xml'],
+    ['xdp', 'application/vnd.adobe.xdp+xml'],
+    ['xdr', 'video/x-amt-demorun'],
+    ['xdssc', 'application/dssc+xml'],
+    ['xdw', 'application/vnd.fujixerox.docuworks'],
+    ['xenc', 'application/xenc+xml'],
+    ['xer', 'application/patch-ops-error+xml'],
+    ['xfdf', 'application/vnd.adobe.xfdf'],
+    ['xfdl', 'application/vnd.xfdl'],
+    ['xgz', 'xgl/drawing'],
+    ['xhtml', 'application/xhtml+xml'],
+    ['xif', 'image/vnd.xiff'],
+    ['xl', 'application/excel'],
+    ['xla', ['application/vnd.ms-excel',
+        'application/excel',
+        'application/x-msexcel',
+        'application/x-excel'
+    ]],
+    ['xlam', 'application/vnd.ms-excel.addin.macroenabled.12'],
+    ['xlb', ['application/excel',
+        'application/vnd.ms-excel',
+        'application/x-excel'
+    ]],
+    ['xlc', ['application/vnd.ms-excel',
+        'application/excel',
+        'application/x-excel'
+    ]],
+    ['xld', ['application/excel', 'application/x-excel']],
+    ['xlk', ['application/excel', 'application/x-excel']],
+    ['xll', ['application/excel',
+        'application/vnd.ms-excel',
+        'application/x-excel'
+    ]],
+    ['xlm', ['application/vnd.ms-excel',
+        'application/excel',
+        'application/x-excel'
+    ]],
+    ['xls', ['application/vnd.ms-excel',
+        'application/excel',
+        'application/x-msexcel',
+        'application/x-excel'
+    ]],
+    ['xlsb',
+        'application/vnd.ms-excel.sheet.binary.macroenabled.12'
+    ],
+    ['xlsm', 'application/vnd.ms-excel.sheet.macroenabled.12'],
+    ['xlsx',
+        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+    ],
+    ['xlt', ['application/vnd.ms-excel',
+        'application/excel',
+        'application/x-excel'
+    ]],
+    ['xltm', 'application/vnd.ms-excel.template.macroenabled.12'],
+    ['xltx',
+        'application/vnd.openxmlformats-officedocument.spreadsheetml.template'
+    ],
+    ['xlv', ['application/excel', 'application/x-excel']],
+    ['xlw', ['application/vnd.ms-excel',
+        'application/excel',
+        'application/x-msexcel',
+        'application/x-excel'
+    ]],
+    ['xm', 'audio/xm'],
+    ['xml', ['application/xml',
+        'text/xml',
+        'application/atom+xml',
+        'application/rss+xml'
+    ]],
+    ['xmz', 'xgl/movie'],
+    ['xo', 'application/vnd.olpc-sugar'],
+    ['xof', 'x-world/x-vrml'],
+    ['xop', 'application/xop+xml'],
+    ['xpi', 'application/x-xpinstall'],
+    ['xpix', 'application/x-vnd.ls-xpix'],
+    ['xpm', ['image/xpm', 'image/x-xpixmap']],
+    ['xpr', 'application/vnd.is-xpr'],
+    ['xps', 'application/vnd.ms-xpsdocument'],
+    ['xpw', 'application/vnd.intercon.formnet'],
+    ['xslt', 'application/xslt+xml'],
+    ['xsm', 'application/vnd.syncml+xml'],
+    ['xspf', 'application/xspf+xml'],
+    ['xsr', 'video/x-amt-showrun'],
+    ['xul', 'application/vnd.mozilla.xul+xml'],
+    ['xwd', ['image/x-xwd', 'image/x-xwindowdump']],
+    ['xyz', ['chemical/x-xyz', 'chemical/x-pdb']],
+    ['yang', 'application/yang'],
+    ['yin', 'application/yin+xml'],
+    ['z', ['application/x-compressed', 'application/x-compress']],
+    ['zaz', 'application/vnd.zzazz.deck+xml'],
+    ['zip', ['application/zip',
+        'multipart/x-zip',
+        'application/x-zip-compressed',
+        'application/x-compressed'
+    ]],
+    ['zir', 'application/vnd.zul'],
+    ['zmm', 'application/vnd.handheld-entertainment+xml'],
+    ['zoo', 'application/octet-stream'],
+    ['zsh', 'text/x-script.zsh']
+]);
+
+module.exports = {
+    detectMimeType(filename) {
+        if (!filename) {
+            return defaultMimeType;
+        }
+
+        let parsed = path.parse(filename);
+        let extension = (parsed.ext.substr(1) || parsed.name || '').split('?').shift().trim().toLowerCase();
+        let value = defaultMimeType;
+
+        if (extensions.has(extension)) {
+            value = extensions.get(extension);
+        }
+
+        if (Array.isArray(value)) {
+            return value[0];
+        }
+        return value;
+    },
+
+    detectExtension(mimeType) {
+        if (!mimeType) {
+            return defaultExtension;
+        }
+        let parts = (mimeType || '').toLowerCase().trim().split('/');
+        let rootType = parts.shift().trim();
+        let subType = parts.join('/').trim();
+
+        if (mimeTypes.has(rootType + '/' + subType)) {
+            let value = mimeTypes.get(rootType + '/' + subType);
+            if (Array.isArray(value)) {
+                return value[0];
+            }
+            return value;
+        }
+
+        switch (rootType) {
+            case 'text':
+                return 'txt';
+            default:
+                return 'bin';
+        }
+    }
+};
diff --git a/node_modules/nodemailer/lib/mime-node/index.js b/node_modules/nodemailer/lib/mime-node/index.js
new file mode 100644
index 0000000..c926f98
--- /dev/null
+++ b/node_modules/nodemailer/lib/mime-node/index.js
@@ -0,0 +1,1200 @@
+/* eslint no-undefined: 0 */
+
+'use strict';
+
+const crypto = require('crypto');
+const os = require('os');
+const fs = require('fs');
+const punycode = require('punycode');
+const PassThrough = require('stream').PassThrough;
+
+const mimeFuncs = require('../mime-funcs');
+const qp = require('../qp');
+const base64 = require('../base64');
+const addressparser = require('../addressparser');
+const fetch = require('../fetch');
+const LastNewline = require('./last-newline');
+
+/**
+ * Creates a new mime tree node. Assumes 'multipart/*' as the content type
+ * if it is a branch, anything else counts as leaf. If rootNode is missing from
+ * the options, assumes this is the root.
+ *
+ * @param {String} contentType Define the content type for the node. Can be left blank for attachments (derived from filename)
+ * @param {Object} [options] optional options
+ * @param {Object} [options.rootNode] root node for this tree
+ * @param {Object} [options.parentNode] immediate parent for this node
+ * @param {Object} [options.filename] filename for an attachment node
+ * @param {String} [options.baseBoundary] shared part of the unique multipart boundary
+ * @param {Boolean} [options.keepBcc] If true, do not exclude Bcc from the generated headers
+ * @param {String} [options.textEncoding] either 'Q' (the default) or 'B'
+ */
+class MimeNode {
+    constructor(contentType, options) {
+        this.nodeCounter = 0;
+
+        options = options || {};
+
+        /**
+         * shared part of the unique multipart boundary
+         */
+        this.baseBoundary = options.baseBoundary || crypto.randomBytes(8).toString('hex');
+        this.boundaryPrefix = options.boundaryPrefix || '--_NmP';
+
+        this.disableFileAccess = !!options.disableFileAccess;
+        this.disableUrlAccess = !!options.disableUrlAccess;
+
+        /**
+         * If date headers is missing and current node is the root, this value is used instead
+         */
+        this.date = new Date();
+
+        /**
+         * Root node for current mime tree
+         */
+        this.rootNode = options.rootNode || this;
+
+        /**
+         * If true include Bcc in generated headers (if available)
+         */
+        this.keepBcc = !!options.keepBcc;
+
+        /**
+         * If filename is specified but contentType is not (probably an attachment)
+         * detect the content type from filename extension
+         */
+        if (options.filename) {
+            /**
+             * Filename for this node. Useful with attachments
+             */
+            this.filename = options.filename;
+            if (!contentType) {
+                contentType = mimeFuncs.detectMimeType(this.filename.split('.').pop());
+            }
+        }
+
+        /**
+         * Indicates which encoding should be used for header strings: "Q" or "B"
+         */
+        this.textEncoding = (options.textEncoding || '').toString().trim().charAt(0).toUpperCase();
+
+        /**
+         * Immediate parent for this node (or undefined if not set)
+         */
+        this.parentNode = options.parentNode;
+
+        /**
+         * Hostname for default message-id values
+         */
+        this.hostname = options.hostname;
+
+        /**
+         * An array for possible child nodes
+         */
+        this.childNodes = [];
+
+        /**
+         * Used for generating unique boundaries (prepended to the shared base)
+         */
+        this._nodeId = ++this.rootNode.nodeCounter;
+
+        /**
+         * A list of header values for this node in the form of [{key:'', value:''}]
+         */
+        this._headers = [];
+
+        /**
+         * True if the content only uses ASCII printable characters
+         * @type {Boolean}
+         */
+        this._isPlainText = false;
+
+        /**
+         * True if the content is plain text but has longer lines than allowed
+         * @type {Boolean}
+         */
+        this._hasLongLines = false;
+
+        /**
+         * If set, use instead this value for envelopes instead of generating one
+         * @type {Boolean}
+         */
+        this._envelope = false;
+
+        /**
+         * If set then use this value as the stream content instead of building it
+         * @type {String|Buffer|Stream}
+         */
+        this._raw = false;
+
+        /**
+         * Additional transform streams that the message will be piped before
+         * exposing by createReadStream
+         * @type {Array}
+         */
+        this._transforms = [];
+
+        /**
+         * Additional process functions that the message will be piped through before
+         * exposing by createReadStream. These functions are run after transforms
+         * @type {Array}
+         */
+        this._processFuncs = [];
+
+        /**
+         * If content type is set (or derived from the filename) add it to headers
+         */
+        if (contentType) {
+            this.setHeader('Content-Type', contentType);
+        }
+    }
+
+    /////// PUBLIC METHODS
+
+    /**
+     * Creates and appends a child node.Arguments provided are passed to MimeNode constructor
+     *
+     * @param {String} [contentType] Optional content type
+     * @param {Object} [options] Optional options object
+     * @return {Object} Created node object
+     */
+    createChild(contentType, options) {
+        if (!options && typeof contentType === 'object') {
+            options = contentType;
+            contentType = undefined;
+        }
+        let node = new MimeNode(contentType, options);
+        this.appendChild(node);
+        return node;
+    }
+
+    /**
+     * Appends an existing node to the mime tree. Removes the node from an existing
+     * tree if needed
+     *
+     * @param {Object} childNode node to be appended
+     * @return {Object} Appended node object
+     */
+    appendChild(childNode) {
+
+        if (childNode.rootNode !== this.rootNode) {
+            childNode.rootNode = this.rootNode;
+            childNode._nodeId = ++this.rootNode.nodeCounter;
+        }
+
+        childNode.parentNode = this;
+
+        this.childNodes.push(childNode);
+        return childNode;
+    }
+
+    /**
+     * Replaces current node with another node
+     *
+     * @param {Object} node Replacement node
+     * @return {Object} Replacement node
+     */
+    replace(node) {
+        if (node === this) {
+            return this;
+        }
+
+        this.parentNode.childNodes.forEach((childNode, i) => {
+            if (childNode === this) {
+
+                node.rootNode = this.rootNode;
+                node.parentNode = this.parentNode;
+                node._nodeId = this._nodeId;
+
+                this.rootNode = this;
+                this.parentNode = undefined;
+
+                node.parentNode.childNodes[i] = node;
+            }
+        });
+
+        return node;
+    }
+
+    /**
+     * Removes current node from the mime tree
+     *
+     * @return {Object} removed node
+     */
+    remove() {
+        if (!this.parentNode) {
+            return this;
+        }
+
+        for (let i = this.parentNode.childNodes.length - 1; i >= 0; i--) {
+            if (this.parentNode.childNodes[i] === this) {
+                this.parentNode.childNodes.splice(i, 1);
+                this.parentNode = undefined;
+                this.rootNode = this;
+                return this;
+            }
+        }
+    }
+
+    /**
+     * Sets a header value. If the value for selected key exists, it is overwritten.
+     * You can set multiple values as well by using [{key:'', value:''}] or
+     * {key: 'value'} as the first argument.
+     *
+     * @param {String|Array|Object} key Header key or a list of key value pairs
+     * @param {String} value Header value
+     * @return {Object} current node
+     */
+    setHeader(key, value) {
+        let added = false,
+            headerValue;
+
+        // Allow setting multiple headers at once
+        if (!value && key && typeof key === 'object') {
+            // allow {key:'content-type', value: 'text/plain'}
+            if (key.key && 'value' in key) {
+                this.setHeader(key.key, key.value);
+            }
+            // allow [{key:'content-type', value: 'text/plain'}]
+            else if (Array.isArray(key)) {
+                key.forEach(i => {
+                    this.setHeader(i.key, i.value);
+                });
+            }
+            // allow {'content-type': 'text/plain'}
+            else {
+                Object.keys(key).forEach(i => {
+                    this.setHeader(i, key[i]);
+                });
+            }
+            return this;
+        }
+
+        key = this._normalizeHeaderKey(key);
+
+        headerValue = {
+            key,
+            value
+        };
+
+        // Check if the value exists and overwrite
+        for (let i = 0, len = this._headers.length; i < len; i++) {
+            if (this._headers[i].key === key) {
+                if (!added) {
+                    // replace the first match
+                    this._headers[i] = headerValue;
+                    added = true;
+                } else {
+                    // remove following matches
+                    this._headers.splice(i, 1);
+                    i--;
+                    len--;
+                }
+            }
+        }
+
+        // match not found, append the value
+        if (!added) {
+            this._headers.push(headerValue);
+        }
+
+        return this;
+    }
+
+    /**
+     * Adds a header value. If the value for selected key exists, the value is appended
+     * as a new field and old one is not touched.
+     * You can set multiple values as well by using [{key:'', value:''}] or
+     * {key: 'value'} as the first argument.
+     *
+     * @param {String|Array|Object} key Header key or a list of key value pairs
+     * @param {String} value Header value
+     * @return {Object} current node
+     */
+    addHeader(key, value) {
+
+        // Allow setting multiple headers at once
+        if (!value && key && typeof key === 'object') {
+            // allow {key:'content-type', value: 'text/plain'}
+            if (key.key && key.value) {
+                this.addHeader(key.key, key.value);
+            }
+            // allow [{key:'content-type', value: 'text/plain'}]
+            else if (Array.isArray(key)) {
+                key.forEach(i => {
+                    this.addHeader(i.key, i.value);
+                });
+            }
+            // allow {'content-type': 'text/plain'}
+            else {
+                Object.keys(key).forEach(i => {
+                    this.addHeader(i, key[i]);
+                });
+            }
+            return this;
+        } else if (Array.isArray(value)) {
+            value.forEach(val => {
+                this.addHeader(key, val);
+            });
+            return this;
+        }
+
+        this._headers.push({
+            key: this._normalizeHeaderKey(key),
+            value
+        });
+
+        return this;
+    }
+
+    /**
+     * Retrieves the first mathcing value of a selected key
+     *
+     * @param {String} key Key to search for
+     * @retun {String} Value for the key
+     */
+    getHeader(key) {
+        key = this._normalizeHeaderKey(key);
+        for (let i = 0, len = this._headers.length; i < len; i++) {
+            if (this._headers[i].key === key) {
+                return this._headers[i].value;
+            }
+        }
+    }
+
+    /**
+     * Sets body content for current node. If the value is a string, charset is added automatically
+     * to Content-Type (if it is text/*). If the value is a Buffer, you need to specify
+     * the charset yourself
+     *
+     * @param (String|Buffer) content Body content
+     * @return {Object} current node
+     */
+    setContent(content) {
+        this.content = content;
+        if (typeof this.content.pipe === 'function') {
+            // pre-stream handler. might be triggered if a stream is set as content
+            // and 'error' fires before anything is done with this stream
+            this._contentErrorHandler = err => {
+                this.content.removeListener('error', this._contentErrorHandler);
+                this.content = err;
+            };
+            this.content.once('error', this._contentErrorHandler);
+        } else if (typeof this.content === 'string') {
+            this._isPlainText = mimeFuncs.isPlainText(this.content);
+            if (this._isPlainText && mimeFuncs.hasLongerLines(this.content, 76)) {
+                // If there are lines longer than 76 symbols/bytes do not use 7bit
+                this._hasLongLines = true;
+            }
+        }
+        return this;
+    }
+
+    build(callback) {
+        let stream = this.createReadStream();
+        let buf = [];
+        let buflen = 0;
+        let returned = false;
+
+        stream.on('readable', () => {
+            let chunk;
+
+            while ((chunk = stream.read()) !== null) {
+                buf.push(chunk);
+                buflen += chunk.length;
+            }
+        });
+
+        stream.once('error', err => {
+            if (returned) {
+                return;
+            }
+            returned = true;
+
+            return callback(err);
+        });
+
+        stream.once('end', chunk => {
+            if (returned) {
+                return;
+            }
+            returned = true;
+
+            if (chunk && chunk.length) {
+                buf.push(chunk);
+                buflen += chunk.length;
+            }
+            return callback(null, Buffer.concat(buf, buflen));
+        });
+    }
+
+    getTransferEncoding() {
+        let transferEncoding = false;
+        let contentType = (this.getHeader('Content-Type') || '').toString().toLowerCase().trim();
+
+        if (this.content) {
+            transferEncoding = (this.getHeader('Content-Transfer-Encoding') || '').toString().toLowerCase().trim();
+            if (!transferEncoding || !['base64', 'quoted-printable'].includes(transferEncoding)) {
+                if (/^text\//i.test(contentType)) {
+                    // If there are no special symbols, no need to modify the text
+                    if (this._isPlainText && !this._hasLongLines) {
+                        transferEncoding = '7bit';
+                    } else if (typeof this.content === 'string' || this.content instanceof Buffer) {
+                        // detect preferred encoding for string value
+                        transferEncoding = this._getTextEncoding(this.content) === 'Q' ? 'quoted-printable' : 'base64';
+                    } else {
+                        // we can not check content for a stream, so either use preferred encoding or fallback to QP
+                        transferEncoding = this.transferEncoding === 'B' ? 'base64' : 'quoted-printable';
+                    }
+                } else if (!/^(multipart|message)\//i.test(contentType)) {
+                    transferEncoding = transferEncoding || 'base64';
+                }
+            }
+        }
+        return transferEncoding;
+    }
+
+    /**
+     * Builds the header block for the mime node. Append \r\n\r\n before writing the content
+     *
+     * @returns {String} Headers
+     */
+    buildHeaders() {
+        let transferEncoding = this.getTransferEncoding();
+        let headers = [];
+
+        if (transferEncoding) {
+            this.setHeader('Content-Transfer-Encoding', transferEncoding);
+        }
+
+        if (this.filename && !this.getHeader('Content-Disposition')) {
+            this.setHeader('Content-Disposition', 'attachment');
+        }
+
+        // Ensure mandatory header fields
+        if (this.rootNode === this) {
+            if (!this.getHeader('Date')) {
+                this.setHeader('Date', this.date.toUTCString().replace(/GMT/, '+0000'));
+            }
+
+            // ensure that Message-Id is present
+            this.messageId();
+
+            if (!this.getHeader('MIME-Version')) {
+                this.setHeader('MIME-Version', '1.0');
+            }
+        }
+
+        this._headers.forEach(header => {
+            let key = header.key;
+            let value = header.value;
+            let structured;
+            let param;
+            let options = {};
+            let formattedHeaders = ['From', 'Sender', 'To', 'Cc', 'Bcc', 'Reply-To', 'Date', 'References'];
+
+            if (value && typeof value === 'object' && !formattedHeaders.includes(key)) {
+                Object.keys(value).forEach(key => {
+                    if (key !== 'value') {
+                        options[key] = value[key];
+                    }
+                });
+                value = (value.value || '').toString();
+                if (!value.trim()) {
+                    return;
+                }
+            }
+
+            if (options.prepared) {
+                // header value is
+                headers.push(key + ': ' + value);
+                return;
+            }
+
+            switch (header.key) {
+                case 'Content-Disposition':
+                    structured = mimeFuncs.parseHeaderValue(value);
+                    if (this.filename) {
+                        structured.params.filename = this.filename;
+                    }
+                    value = mimeFuncs.buildHeaderValue(structured);
+                    break;
+                case 'Content-Type':
+                    structured = mimeFuncs.parseHeaderValue(value);
+
+                    this._handleContentType(structured);
+
+                    if (structured.value.match(/^text\/plain\b/) && typeof this.content === 'string' && /[\u0080-\uFFFF]/.test(this.content)) {
+                        structured.params.charset = 'utf-8';
+                    }
+
+                    value = mimeFuncs.buildHeaderValue(structured);
+
+                    if (this.filename) {
+                        // add support for non-compliant clients like QQ webmail
+                        // we can't build the value with buildHeaderValue as the value is non standard and
+                        // would be converted to parameter continuation encoding that we do not want
+                        param = this._encodeWords(this.filename);
+
+                        if (param !== this.filename || /[\s'"\\;:\/=\(\),<>@\[\]\?]|^\-/.test(param)) {
+                            // include value in quotes if needed
+                            param = '"' + param + '"';
+                        }
+                        value += '; name=' + param;
+                    }
+                    break;
+                case 'Bcc':
+                    if (!this.keepBcc) {
+                        // skip BCC values
+                        return;
+                    }
+                    break;
+            }
+
+            value = this._encodeHeaderValue(key, value);
+
+            // skip empty lines
+            if (!(value || '').toString().trim()) {
+                return;
+            }
+
+            headers.push(mimeFuncs.foldLines(key + ': ' + value, 76));
+        });
+
+        return headers.join('\r\n');
+    }
+
+    /**
+     * Streams the rfc2822 message from the current node. If this is a root node,
+     * mandatory header fields are set if missing (Date, Message-Id, MIME-Version)
+     *
+     * @return {String} Compiled message
+     */
+    createReadStream(options) {
+        options = options || {};
+
+        let stream = new PassThrough(options);
+        let outputStream = stream;
+        let transform;
+
+        this.stream(stream, options, err => {
+            if (err) {
+                outputStream.emit('error', err);
+                return;
+            }
+            stream.end();
+        });
+
+        for (let i = 0, len = this._transforms.length; i < len; i++) {
+            transform = typeof this._transforms[i] === 'function' ? this._transforms[i]() : this._transforms[i];
+            outputStream.once('error', err => {
+                transform.emit('error', err);
+            });
+            outputStream = outputStream.pipe(transform);
+        }
+
+        // ensure terminating newline after possible user transforms
+        transform = new LastNewline();
+        outputStream.once('error', err => {
+            transform.emit('error', err);
+        });
+        outputStream = outputStream.pipe(transform);
+
+        // dkim and stuff
+        for (let i = 0, len = this._processFuncs.length; i < len; i++) {
+            transform = this._processFuncs[i];
+            outputStream = transform(outputStream);
+        }
+
+        return outputStream;
+    }
+
+    /**
+     * Appends a transform stream object to the transforms list. Final output
+     * is passed through this stream before exposing
+     *
+     * @param {Object} transform Read-Write stream
+     */
+    transform(transform) {
+        this._transforms.push(transform);
+    }
+
+    /**
+     * Appends a post process function. The functon is run after transforms and
+     * uses the following syntax
+     *
+     *   processFunc(input) -> outputStream
+     *
+     * @param {Object} processFunc Read-Write stream
+     */
+    processFunc(processFunc) {
+        this._processFuncs.push(processFunc);
+    }
+
+    stream(outputStream, options, done) {
+        let transferEncoding = this.getTransferEncoding();
+        let contentStream;
+        let localStream;
+
+        // protect actual callback against multiple triggering
+        let returned = false;
+        let callback = err => {
+            if (returned) {
+                return;
+            }
+            returned = true;
+            done(err);
+        };
+
+        // for multipart nodes, push child nodes
+        // for content nodes end the stream
+        let finalize = () => {
+            let childId = 0;
+            let processChildNode = () => {
+                if (childId >= this.childNodes.length) {
+                    outputStream.write('\r\n--' + this.boundary + '--\r\n');
+                    return callback();
+                }
+                let child = this.childNodes[childId++];
+                outputStream.write((childId > 1 ? '\r\n' : '') + '--' + this.boundary + '\r\n');
+                child.stream(outputStream, options, err => {
+                    if (err) {
+                        return callback(err);
+                    }
+                    setImmediate(processChildNode);
+                });
+            };
+
+            if (this.multipart) {
+                setImmediate(processChildNode);
+            } else {
+                return callback();
+            }
+        };
+
+        // pushes node content
+        let sendContent = () => {
+            if (this.content) {
+
+                if (Object.prototype.toString.call(this.content) === '[object Error]') {
+                    // content is already errored
+                    return callback(this.content);
+                }
+
+                if (typeof this.content.pipe === 'function') {
+                    this.content.removeListener('error', this._contentErrorHandler);
+                    this._contentErrorHandler = err => callback(err);
+                    this.content.once('error', this._contentErrorHandler);
+                }
+
+                let createStream = () => {
+
+                    if (['quoted-printable', 'base64'].includes(transferEncoding)) {
+                        contentStream = new(transferEncoding === 'base64' ? base64 : qp).Encoder(options);
+
+                        contentStream.pipe(outputStream, {
+                            end: false
+                        });
+                        contentStream.once('end', finalize);
+                        contentStream.once('error', err => callback(err));
+
+                        localStream = this._getStream(this.content);
+                        localStream.pipe(contentStream);
+                    } else {
+                        // anything that is not QP or Base54 passes as-is
+                        localStream = this._getStream(this.content);
+                        localStream.pipe(outputStream, {
+                            end: false
+                        });
+                        localStream.once('end', finalize);
+                    }
+
+                    localStream.once('error', err => callback(err));
+                };
+
+                if (this.content._resolve) {
+                    let chunks = [];
+                    let chunklen = 0;
+                    let returned = false;
+                    let sourceStream = this._getStream(this.content);
+                    sourceStream.on('error', err => {
+                        if (returned) {
+                            return;
+                        }
+                        returned = true;
+                        callback(err);
+                    });
+                    sourceStream.on('readable', () => {
+                        let chunk;
+                        while ((chunk = sourceStream.read()) !== null) {
+                            chunks.push(chunk);
+                            chunklen += chunk.length;
+                        }
+                    });
+                    sourceStream.on('end', () => {
+                        if (returned) {
+                            return;
+                        }
+                        returned = true;
+                        this.content._resolve = false;
+                        this.content._resolvedValue = Buffer.concat(chunks, chunklen);
+                        setImmediate(createStream);
+                    });
+                } else {
+                    setImmediate(createStream);
+                }
+                return;
+            } else {
+                return setImmediate(finalize);
+            }
+        };
+
+        if (this._raw) {
+            setImmediate(() => {
+                if (Object.prototype.toString.call(this._raw) === '[object Error]') {
+                    // content is already errored
+                    return callback(this._raw);
+                }
+
+                // remove default error handler (if set)
+                if (typeof this._raw.pipe === 'function') {
+                    this._raw.removeListener('error', this._contentErrorHandler);
+                }
+
+                let raw = this._getStream(this._raw);
+                raw.pipe(outputStream, {
+                    end: false
+                });
+                raw.on('error', err => outputStream.emit('error', err));
+                raw.on('end', finalize);
+            });
+        } else {
+            outputStream.write(this.buildHeaders() + '\r\n\r\n');
+            setImmediate(sendContent);
+        }
+    }
+
+    /**
+     * Sets envelope to be used instead of the generated one
+     *
+     * @return {Object} SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
+     */
+    setEnvelope(envelope) {
+        let list;
+
+        this._envelope = {
+            from: false,
+            to: []
+        };
+
+        if (envelope.from) {
+            list = [];
+            this._convertAddresses(this._parseAddresses(envelope.from), list);
+            list = list.filter(address => address && address.address);
+            if (list.length && list[0]) {
+                this._envelope.from = list[0].address;
+            }
+        }
+        ['to', 'cc', 'bcc'].forEach(key => {
+            if (envelope[key]) {
+                this._convertAddresses(this._parseAddresses(envelope[key]), this._envelope.to);
+            }
+        });
+
+        this._envelope.to = this._envelope.to.map(to => to.address).filter(address => address);
+
+        let standardFields = ['to', 'cc', 'bcc', 'from'];
+        Object.keys(envelope).forEach(key => {
+            if (!standardFields.includes(key)) {
+                this._envelope[key] = envelope[key];
+            }
+        });
+
+        return this;
+    }
+
+    /**
+     * Generates and returns an object with parsed address fields
+     *
+     * @return {Object} Address object
+     */
+    getAddresses() {
+        let addresses = {};
+
+        this._headers.forEach(header => {
+            let key = header.key.toLowerCase();
+            if (['from', 'sender', 'reply-to', 'to', 'cc', 'bcc'].includes(key)) {
+                if (!Array.isArray(addresses[key])) {
+                    addresses[key] = [];
+                }
+
+                this._convertAddresses(this._parseAddresses(header.value), addresses[key]);
+            }
+        });
+
+        return addresses;
+    }
+
+    /**
+     * Generates and returns SMTP envelope with the sender address and a list of recipients addresses
+     *
+     * @return {Object} SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
+     */
+    getEnvelope() {
+        if (this._envelope) {
+            return this._envelope;
+        }
+
+        let envelope = {
+            from: false,
+            to: []
+        };
+        this._headers.forEach(header => {
+            let list = [];
+            if (header.key === 'From' || (!envelope.from && ['Reply-To', 'Sender'].includes(header.key))) {
+                this._convertAddresses(this._parseAddresses(header.value), list);
+                if (list.length && list[0]) {
+                    envelope.from = list[0].address;
+                }
+            } else if (['To', 'Cc', 'Bcc'].includes(header.key)) {
+                this._convertAddresses(this._parseAddresses(header.value), envelope.to);
+            }
+        });
+
+        envelope.to = envelope.to.map(to => to.address);
+
+        return envelope;
+    }
+
+    /**
+     * Returns Message-Id value. If it does not exist, then creates one
+     *
+     * @return {String} Message-Id value
+     */
+    messageId() {
+        let messageId = this.getHeader('Message-ID');
+        // You really should define your own Message-Id field!
+        if (!messageId) {
+            messageId = this._generateMessageId();
+            this.setHeader('Message-ID', messageId);
+        }
+        return messageId;
+    }
+
+    /**
+     * Sets pregenerated content that will be used as the output of this node
+     *
+     * @param {String|Buffer|Stream} Raw MIME contents
+     */
+    setRaw(raw) {
+        this._raw = raw;
+
+        if (this._raw && typeof this._raw.pipe === 'function') {
+            // pre-stream handler. might be triggered if a stream is set as content
+            // and 'error' fires before anything is done with this stream
+            this._contentErrorHandler = err => {
+                this._raw.removeListener('error', this._contentErrorHandler);
+                this._raw = err;
+            };
+            this._raw.once('error', this._contentErrorHandler);
+        }
+
+        return this;
+    }
+
+    /////// PRIVATE METHODS
+
+    /**
+     * Detects and returns handle to a stream related with the content.
+     *
+     * @param {Mixed} content Node content
+     * @returns {Object} Stream object
+     */
+    _getStream(content) {
+        let contentStream;
+
+        if (content._resolvedValue) {
+            // pass string or buffer content as a stream
+            contentStream = new PassThrough();
+            setImmediate(() => contentStream.end(content._resolvedValue));
+            return contentStream;
+        } else if (typeof content.pipe === 'function') {
+            // assume as stream
+            return content;
+        } else if (content && typeof content.path === 'string' && !content.href) {
+            if (this.disableFileAccess) {
+                contentStream = new PassThrough();
+                setImmediate(() => contentStream.emit('error', new Error('File access rejected for ' + content.path)));
+                return contentStream;
+            }
+            // read file
+            return fs.createReadStream(content.path);
+        } else if (content && typeof content.href === 'string') {
+            if (this.disableUrlAccess) {
+                contentStream = new PassThrough();
+                setImmediate(() => contentStream.emit('error', new Error('Url access rejected for ' + content.href)));
+                return contentStream;
+            }
+            // fetch URL
+            return fetch(content.href);
+        } else {
+            // pass string or buffer content as a stream
+            contentStream = new PassThrough();
+            setImmediate(() => contentStream.end(content || ''));
+            return contentStream;
+        }
+    }
+
+    /**
+     * Parses addresses. Takes in a single address or an array or an
+     * array of address arrays (eg. To: [[first group], [second group],...])
+     *
+     * @param {Mixed} addresses Addresses to be parsed
+     * @return {Array} An array of address objects
+     */
+    _parseAddresses(addresses) {
+        return [].concat.apply([], [].concat(addresses).map(address => { // eslint-disable-line prefer-spread
+            if (address && address.address) {
+                address.address = this._normalizeAddress(address.address);
+                address.name = address.name || '';
+                return [address];
+            }
+            return addressparser(address);
+        }));
+    }
+
+    /**
+     * Normalizes a header key, uses Camel-Case form, except for uppercase MIME-
+     *
+     * @param {String} key Key to be normalized
+     * @return {String} key in Camel-Case form
+     */
+    _normalizeHeaderKey(key) {
+        return (key || '').toString().
+        // no newlines in keys
+        replace(/\r?\n|\r/g, ' ').
+        trim().toLowerCase().
+        // use uppercase words, except MIME
+        replace(/^X\-SMTPAPI$|^(MIME|DKIM)\b|^[a-z]|\-(SPF|FBL|ID|MD5)$|\-[a-z]/ig, c => c.toUpperCase()).
+        // special case
+        replace(/^Content\-Features$/i, 'Content-features');
+    }
+
+    /**
+     * Checks if the content type is multipart and defines boundary if needed.
+     * Doesn't return anything, modifies object argument instead.
+     *
+     * @param {Object} structured Parsed header value for 'Content-Type' key
+     */
+    _handleContentType(structured) {
+        this.contentType = structured.value.trim().toLowerCase();
+
+        this.multipart = this.contentType.split('/').reduce((prev, value) => prev === 'multipart' ? value : false);
+
+        if (this.multipart) {
+            this.boundary = structured.params.boundary = structured.params.boundary || this.boundary || this._generateBoundary();
+        } else {
+            this.boundary = false;
+        }
+    }
+
+    /**
+     * Generates a multipart boundary value
+     *
+     * @return {String} boundary value
+     */
+    _generateBoundary() {
+        return this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary + '-Part_' + this._nodeId;
+    }
+
+    /**
+     * Encodes a header value for use in the generated rfc2822 email.
+     *
+     * @param {String} key Header key
+     * @param {String} value Header value
+     */
+    _encodeHeaderValue(key, value) {
+        key = this._normalizeHeaderKey(key);
+
+        switch (key) {
+
+            // Structured headers
+            case 'From':
+            case 'Sender':
+            case 'To':
+            case 'Cc':
+            case 'Bcc':
+            case 'Reply-To':
+                return this._convertAddresses(this._parseAddresses(value));
+
+                // values enclosed in <>
+            case 'Message-ID':
+            case 'In-Reply-To':
+            case 'Content-Id':
+                value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
+
+                if (value.charAt(0) !== '<') {
+                    value = '<' + value;
+                }
+
+                if (value.charAt(value.length - 1) !== '>') {
+                    value = value + '>';
+                }
+                return value;
+
+                // space separated list of values enclosed in <>
+            case 'References':
+                value = [].concat.apply([], [].concat(value || '').map(elm => { // eslint-disable-line prefer-spread
+                    elm = (elm || '').toString().replace(/\r?\n|\r/g, ' ').trim();
+                    return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/);
+                })).map(elm => {
+                    if (elm.charAt(0) !== '<') {
+                        elm = '<' + elm;
+                    }
+                    if (elm.charAt(elm.length - 1) !== '>') {
+                        elm = elm + '>';
+                    }
+                    return elm;
+                });
+
+                return value.join(' ').trim();
+
+            case 'Date':
+                if (Object.prototype.toString.call(value) === '[object Date]') {
+                    return value.toUTCString().replace(/GMT/, '+0000');
+                }
+
+                value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
+                return this._encodeWords(value);
+
+            default:
+                value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
+                // encodeWords only encodes if needed, otherwise the original string is returned
+                return this._encodeWords(value);
+        }
+    }
+
+    /**
+     * Rebuilds address object using punycode and other adjustments
+     *
+     * @param {Array} addresses An array of address objects
+     * @param {Array} [uniqueList] An array to be populated with addresses
+     * @return {String} address string
+     */
+    _convertAddresses(addresses, uniqueList) {
+        let values = [];
+
+        uniqueList = uniqueList || [];
+
+        [].concat(addresses || []).forEach(address => {
+            if (address.address) {
+                address.address = this._normalizeAddress(address.address);
+
+                if (!address.name) {
+                    values.push(address.address);
+                } else if (address.name) {
+                    values.push(this._encodeAddressName(address.name) + ' <' + address.address + '>');
+                }
+
+                if (address.address) {
+                    if (!uniqueList.filter(a => a.address === address.address).length) {
+                        uniqueList.push(address);
+                    }
+                }
+            } else if (address.group) {
+                values.push(this._encodeAddressName(address.name) + ':' + (address.group.length ? this._convertAddresses(address.group, uniqueList) : '').trim() + ';');
+            }
+        });
+
+        return values.join(', ');
+    }
+
+    /**
+     * Normalizes an email address
+     *
+     * @param {Array} address An array of address objects
+     * @return {String} address string
+     */
+    _normalizeAddress(address) {
+        address = (address || '').toString().trim();
+
+        let lastAt = address.lastIndexOf('@');
+        let user = address.substr(0, lastAt);
+        let domain = address.substr(lastAt + 1);
+
+        // Usernames are not touched and are kept as is even if these include unicode
+        // Domains are punycoded by default
+        // 'jõgeva.ee' will be converted to 'xn--jgeva-dua.ee'
+        // non-unicode domains are left as is
+
+        return user + '@' + punycode.toASCII(domain.toLowerCase());
+    }
+
+    /**
+     * If needed, mime encodes the name part
+     *
+     * @param {String} name Name part of an address
+     * @returns {String} Mime word encoded string if needed
+     */
+    _encodeAddressName(name) {
+        if (!/^[\w ']*$/.test(name)) {
+            if (/^[\x20-\x7e]*$/.test(name)) {
+                return '"' + name.replace(/([\\"])/g, '\\$1') + '"';
+            } else {
+                return mimeFuncs.encodeWord(name, this._getTextEncoding(name), 52);
+            }
+        }
+        return name;
+    }
+
+    /**
+     * If needed, mime encodes the name part
+     *
+     * @param {String} name Name part of an address
+     * @returns {String} Mime word encoded string if needed
+     */
+    _encodeWords(value) {
+        return mimeFuncs.encodeWords(value, this._getTextEncoding(value), 52);
+    }
+
+    /**
+     * Detects best mime encoding for a text value
+     *
+     * @param {String} value Value to check for
+     * @return {String} either 'Q' or 'B'
+     */
+    _getTextEncoding(value) {
+        value = (value || '').toString();
+
+        let encoding = this.textEncoding;
+        let latinLen;
+        let nonLatinLen;
+
+        if (!encoding) {
+            // count latin alphabet symbols and 8-bit range symbols + control symbols
+            // if there are more latin characters, then use quoted-printable
+            // encoding, otherwise use base64
+            nonLatinLen = (value.match(/[\x00-\x08\x0B\x0C\x0E-\x1F\u0080-\uFFFF]/g) || []).length; // eslint-disable-line no-control-regex
+            latinLen = (value.match(/[a-z]/gi) || []).length;
+            // if there are more latin symbols than binary/unicode, then prefer Q, otherwise B
+            encoding = nonLatinLen < latinLen ? 'Q' : 'B';
+        }
+        return encoding;
+    }
+
+    /**
+     * Generates a message id
+     *
+     * @return {String} Random Message-ID value
+     */
+    _generateMessageId() {
+        return '<' + [2, 2, 2, 6].reduce(
+                // crux to generate UUID-like random strings
+                (prev, len) => prev + '-' + crypto.randomBytes(len).toString('hex'),
+                crypto.randomBytes(4).toString('hex')) + '@' +
+            // try to use the domain of the FROM address or fallback to server hostname
+            (this.getEnvelope().from || this.hostname || os.hostname() || 'localhost').split('@').pop() + '>';
+    }
+}
+
+module.exports = MimeNode;
diff --git a/node_modules/nodemailer/lib/mime-node/last-newline.js b/node_modules/nodemailer/lib/mime-node/last-newline.js
new file mode 100644
index 0000000..42b011e
--- /dev/null
+++ b/node_modules/nodemailer/lib/mime-node/last-newline.js
@@ -0,0 +1,33 @@
+'use strict';
+
+const Transform = require('stream').Transform;
+
+class LastNewline extends Transform {
+    constructor() {
+        super();
+        this.lastByte = false;
+    }
+
+    _transform(chunk, encoding, done) {
+        if (chunk.length) {
+            this.lastByte = chunk[chunk.length - 1];
+        }
+
+        this.push(chunk);
+        done();
+    }
+
+    _flush(done) {
+        if (this.lastByte === 0x0A) {
+            return done();
+        }
+        if (this.lastByte === 0x0D) {
+            this.push(Buffer.from('\n'));
+            return done();
+        }
+        this.push(Buffer.from('\r\n'));
+        return done();
+    }
+}
+
+module.exports = LastNewline;
diff --git a/node_modules/nodemailer/lib/nodemailer.js b/node_modules/nodemailer/lib/nodemailer.js
new file mode 100644
index 0000000..e3bdb5e
--- /dev/null
+++ b/node_modules/nodemailer/lib/nodemailer.js
@@ -0,0 +1,49 @@
+'use strict';
+
+const Mailer = require('./mailer');
+const shared = require('./shared');
+const SMTPPool = require('./smtp-pool');
+const SMTPTransport = require('./smtp-transport');
+const SendmailTransport = require('./sendmail-transport');
+const StreamTransport = require('./stream-transport');
+const JSONTransport = require('./json-transport');
+const SESTransport = require('./ses-transport');
+
+module.exports.createTransport = function (transporter, defaults) {
+    let urlConfig;
+    let options;
+    let mailer;
+
+    if (
+        // provided transporter is a configuration object, not transporter plugin
+        (typeof transporter === 'object' && typeof transporter.send !== 'function') ||
+        // provided transporter looks like a connection url
+        (typeof transporter === 'string' && /^(smtps?|direct):/i.test(transporter))
+    ) {
+
+        if ((urlConfig = typeof transporter === 'string' ? transporter : transporter.url)) {
+            // parse a configuration URL into configuration options
+            options = shared.parseConnectionUrl(urlConfig);
+        } else {
+            options = transporter;
+        }
+
+        if (options.pool) {
+            transporter = new SMTPPool(options);
+        } else if (options.sendmail) {
+            transporter = new SendmailTransport(options);
+        } else if (options.streamTransport) {
+            transporter = new StreamTransport(options);
+        } else if (options.jsonTransport) {
+            transporter = new JSONTransport(options);
+        } else if (options.SES) {
+            transporter = new SESTransport(options);
+        } else {
+            transporter = new SMTPTransport(options);
+        }
+    }
+
+    mailer = new Mailer(transporter, options, defaults);
+
+    return mailer;
+};
diff --git a/node_modules/nodemailer/lib/qp/index.js b/node_modules/nodemailer/lib/qp/index.js
new file mode 100644
index 0000000..742b01e
--- /dev/null
+++ b/node_modules/nodemailer/lib/qp/index.js
@@ -0,0 +1,221 @@
+'use strict';
+
+const Transform = require('stream').Transform;
+
+/**
+ * Encodes a Buffer into a Quoted-Printable encoded string
+ *
+ * @param {Buffer} buffer Buffer to convert
+ * @returns {String} Quoted-Printable encoded string
+ */
+function encode(buffer) {
+    if (typeof buffer === 'string') {
+        buffer = new Buffer(buffer, 'utf-8');
+    }
+
+    // usable characters that do not need encoding
+    let ranges = [
+        // https://tools.ietf.org/html/rfc2045#section-6.7
+        [0x09], // <TAB>
+        [0x0A], // <LF>
+        [0x0D], // <CR>
+        [0x20, 0x3C], // <SP>!"#$%&'()*+,-./0123456789:;
+        [0x3E, 0x7E] // >?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}
+    ];
+    let result = '';
+    let ord;
+
+    for (let i = 0, len = buffer.length; i < len; i++) {
+        ord = buffer[i];
+        // if the char is in allowed range, then keep as is, unless it is a WS in the end of a line
+        if (checkRanges(ord, ranges) && !((ord === 0x20 || ord === 0x09) && (i === len - 1 || buffer[i + 1] === 0x0a || buffer[i + 1] === 0x0d))) {
+            result += String.fromCharCode(ord);
+            continue;
+        }
+        result += '=' + (ord < 0x10 ? '0' : '') + ord.toString(16).toUpperCase();
+    }
+
+    return result;
+}
+
+/**
+ * Adds soft line breaks to a Quoted-Printable string
+ *
+ * @param {String} str Quoted-Printable encoded string that might need line wrapping
+ * @param {Number} [lineLength=76] Maximum allowed length for a line
+ * @returns {String} Soft-wrapped Quoted-Printable encoded string
+ */
+function wrap(str, lineLength) {
+    str = (str || '').toString();
+    lineLength = lineLength || 76;
+
+    if (str.length <= lineLength) {
+        return str;
+    }
+
+    let pos = 0;
+    let len = str.length;
+    let match, code, line;
+    let lineMargin = Math.floor(lineLength / 3);
+    let result = '';
+
+    // insert soft linebreaks where needed
+    while (pos < len) {
+        line = str.substr(pos, lineLength);
+        if ((match = line.match(/\r\n/))) {
+            line = line.substr(0, match.index + match[0].length);
+            result += line;
+            pos += line.length;
+            continue;
+        }
+
+        if (line.substr(-1) === '\n') {
+            // nothing to change here
+            result += line;
+            pos += line.length;
+            continue;
+        } else if ((match = line.substr(-lineMargin).match(/\n.*?$/))) {
+            // truncate to nearest line break
+            line = line.substr(0, line.length - (match[0].length - 1));
+            result += line;
+            pos += line.length;
+            continue;
+        } else if (line.length > lineLength - lineMargin && (match = line.substr(-lineMargin).match(/[ \t\.,!\?][^ \t\.,!\?]*$/))) {
+            // truncate to nearest space
+            line = line.substr(0, line.length - (match[0].length - 1));
+        } else if (line.match(/\=[\da-f]{0,2}$/i)) {
+
+            // push incomplete encoding sequences to the next line
+            if ((match = line.match(/\=[\da-f]{0,1}$/i))) {
+                line = line.substr(0, line.length - match[0].length);
+            }
+
+            // ensure that utf-8 sequences are not split
+            while (line.length > 3 && line.length < len - pos && !line.match(/^(?:=[\da-f]{2}){1,4}$/i) && (match = line.match(/\=[\da-f]{2}$/ig))) {
+                code = parseInt(match[0].substr(1, 2), 16);
+                if (code < 128) {
+                    break;
+                }
+
+                line = line.substr(0, line.length - 3);
+
+                if (code >= 0xC0) {
+                    break;
+                }
+            }
+        }
+
+        if (pos + line.length < len && line.substr(-1) !== '\n') {
+            if (line.length === lineLength && line.match(/\=[\da-f]{2}$/i)) {
+                line = line.substr(0, line.length - 3);
+            } else if (line.length === lineLength) {
+                line = line.substr(0, line.length - 1);
+            }
+            pos += line.length;
+            line += '=\r\n';
+        } else {
+            pos += line.length;
+        }
+
+        result += line;
+    }
+
+    return result;
+}
+
+/**
+ * Helper function to check if a number is inside provided ranges
+ *
+ * @param {Number} nr Number to check for
+ * @param {Array} ranges An Array of allowed values
+ * @returns {Boolean} True if the value was found inside allowed ranges, false otherwise
+ */
+function checkRanges(nr, ranges) {
+    for (let i = ranges.length - 1; i >= 0; i--) {
+        if (!ranges[i].length) {
+            continue;
+        }
+        if (ranges[i].length === 1 && nr === ranges[i][0]) {
+            return true;
+        }
+        if (ranges[i].length === 2 && nr >= ranges[i][0] && nr <= ranges[i][1]) {
+            return true;
+        }
+    }
+    return false;
+}
+
+/**
+ * Creates a transform stream for encoding data to Quoted-Printable encoding
+ *
+ * @constructor
+ * @param {Object} options Stream options
+ * @param {Number} [options.lineLength=76] Maximum lenght for lines, set to false to disable wrapping
+ */
+class Encoder extends Transform {
+    constructor(options) {
+        super();
+
+        // init Transform
+        this.options = options || {};
+
+        if (this.options.lineLength !== false) {
+            this.options.lineLength = this.options.lineLength || 76;
+        }
+
+        this._curLine = '';
+
+        this.inputBytes = 0;
+        this.outputBytes = 0;
+    }
+
+    _transform(chunk, encoding, done) {
+        let qp;
+
+        if (encoding !== 'buffer') {
+            chunk = new Buffer(chunk, encoding);
+        }
+
+        if (!chunk || !chunk.length) {
+            return done();
+        }
+
+        this.inputBytes += chunk.length;
+
+        if (this.options.lineLength) {
+            qp = this._curLine + encode(chunk);
+            qp = wrap(qp, this.options.lineLength);
+            qp = qp.replace(/(^|\n)([^\n]*)$/, (match, lineBreak, lastLine) => {
+                this._curLine = lastLine;
+                return lineBreak;
+            });
+
+            if (qp) {
+                this.outputBytes += qp.length;
+                this.push(qp);
+            }
+
+        } else {
+            qp = encode(chunk);
+            this.outputBytes += qp.length;
+            this.push(qp, 'ascii');
+        }
+
+        done();
+    }
+
+    _flush(done) {
+        if (this._curLine) {
+            this.outputBytes += this._curLine.length;
+            this.push(this._curLine, 'ascii');
+        }
+        done();
+    }
+}
+
+// expose to the world
+module.exports = {
+    encode,
+    wrap,
+    Encoder
+};
diff --git a/node_modules/nodemailer/lib/sendmail-transport/index.js b/node_modules/nodemailer/lib/sendmail-transport/index.js
new file mode 100644
index 0000000..d18ce65
--- /dev/null
+++ b/node_modules/nodemailer/lib/sendmail-transport/index.js
@@ -0,0 +1,180 @@
+'use strict';
+
+const spawn = require('child_process').spawn;
+const packageData = require('../../package.json');
+const LeWindows = require('./le-windows');
+const LeUnix = require('./le-unix');
+const shared = require('../shared');
+
+/**
+ * Generates a Transport object for Sendmail
+ *
+ * Possible options can be the following:
+ *
+ *  * **path** optional path to sendmail binary
+ *  * **newline** either 'windows' or 'unix'
+ *  * **args** an array of arguments for the sendmail binary
+ *
+ * @constructor
+ * @param {Object} optional config parameter for the AWS Sendmail service
+ */
+class SendmailTransport {
+    constructor(options) {
+        options = options || {};
+
+        // use a reference to spawn for mocking purposes
+        this._spawn = spawn;
+
+        this.options = options || {};
+
+        this.name = 'Sendmail';
+        this.version = packageData.version;
+
+        this.path = 'sendmail';
+        this.args = false;
+        this.winbreak = false;
+
+        this.logger = shared.getLogger(this.options, {
+            component: this.options.component || 'sendmail'
+        });
+
+        if (options) {
+            if (typeof options === 'string') {
+                this.path = options;
+            } else if (typeof options === 'object') {
+                if (options.path) {
+                    this.path = options.path;
+                }
+                if (Array.isArray(options.args)) {
+                    this.args = options.args;
+                }
+                this.winbreak = ['win', 'windows', 'dos', '\r\n'].includes((options.newline || '').toString().toLowerCase());
+            }
+        }
+    }
+
+    /**
+     * <p>Compiles a mailcomposer message and forwards it to handler that sends it.</p>
+     *
+     * @param {Object} emailMessage MailComposer object
+     * @param {Function} callback Callback function to run when the sending is completed
+     */
+    send(mail, done) {
+        // Sendmail strips this header line by itself
+        mail.message.keepBcc = true;
+
+        let envelope = mail.data.envelope || mail.message.getEnvelope();
+        let messageId = mail.message.messageId();
+        let args;
+        let sendmail;
+        let returned;
+        let transform;
+
+        if (this.args) {
+            // force -i to keep single dots
+            args = ['-i'].concat(this.args).concat(envelope.to);
+        } else {
+            args = ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);
+        }
+
+        let callback = err => {
+            if (returned) {
+                // ignore any additional responses, already done
+                return;
+            }
+            returned = true;
+            if (typeof done === 'function') {
+                if (err) {
+                    return done(err);
+                } else {
+                    return done(null, {
+                        envelope: mail.data.envelope || mail.message.getEnvelope(),
+                        messageId,
+                        response: 'Messages queued for delivery'
+                    });
+                }
+            }
+        };
+
+        try {
+            sendmail = this._spawn(this.path, args);
+        } catch (E) {
+            this.logger.error({
+                err: E,
+                tnx: 'spawn',
+                messageId
+            }, 'Error occurred while spawning sendmail. %s', E.message);
+            return callback(E);
+        }
+
+        if (sendmail) {
+            sendmail.on('error', err => {
+                this.logger.error({
+                    err,
+                    tnx: 'spawn',
+                    messageId
+                }, 'Error occurred when sending message %s. %s', messageId, err.message);
+                callback(err);
+            });
+
+            sendmail.once('exit', code => {
+                if (!code) {
+                    return callback();
+                }
+                let err;
+                if (code === 127) {
+                    err = new Error('Sendmail command not found, process exited with code ' + code);
+                } else {
+                    err = new Error('Sendmail exited with code ' + code);
+                }
+
+                this.logger.error({
+                    err,
+                    tnx: 'stdin',
+                    messageId
+                }, 'Error sending message %s to sendmail. %s', messageId, err.message);
+                callback(err);
+            });
+            sendmail.once('close', callback);
+
+            sendmail.stdin.on('error', err => {
+                this.logger.error({
+                    err,
+                    tnx: 'stdin',
+                    messageId
+                }, 'Error occurred when piping message %s to sendmail. %s', messageId, err.message);
+                callback(err);
+            });
+
+            let recipients = [].concat(envelope.to || []);
+            if (recipients.length > 3) {
+                recipients.push('...and ' + recipients.splice(2).length + ' more');
+            }
+            this.logger.info({
+                tnx: 'send',
+                messageId
+            }, 'Sending message %s to <%s>', messageId, recipients.join(', '));
+
+            transform = this.winbreak ? new LeWindows() : new LeUnix();
+            let sourceStream = mail.message.createReadStream();
+
+            transform.once('error', err => {
+                this.logger.error({
+                    err,
+                    tnx: 'stdin',
+                    messageId
+                }, 'Error occurred when generating message %s. %s', messageId, err.message);
+                sendmail.kill('SIGINT'); // do not deliver the message
+                callback(err);
+            });
+
+            sourceStream.once('error', err => transform.emit('error', err));
+            sourceStream.pipe(transform).pipe(sendmail.stdin);
+        } else {
+            return callback(new Error('sendmail was not found'));
+        }
+
+    }
+}
+
+module.exports = SendmailTransport;
diff --git a/node_modules/nodemailer/lib/sendmail-transport/le-unix.js b/node_modules/nodemailer/lib/sendmail-transport/le-unix.js
new file mode 100644
index 0000000..46fdb36
--- /dev/null
+++ b/node_modules/nodemailer/lib/sendmail-transport/le-unix.js
@@ -0,0 +1,43 @@
+'use strict';
+
+const stream = require('stream');
+const Transform = stream.Transform;
+
+/**
+ * Ensures that only <LF> is used for linebreaks
+ *
+ * @param {Object} options Stream options
+ */
+class LeWindows extends Transform {
+
+    constructor(options) {
+        super(options);
+        // init Transform
+        this.options = options || {};
+    }
+
+    /**
+     * Escapes dots
+     */
+    _transform(chunk, encoding, done) {
+        let buf;
+        let lastPos = 0;
+
+        for (let i = 0, len = chunk.length; i < len; i++) {
+            if (chunk[i] === 0x0D) { // \n
+                buf = chunk.slice(lastPos, i);
+                lastPos = i + 1;
+                this.push(buf);
+            }
+        }
+        if (lastPos && lastPos < chunk.length) {
+            buf = chunk.slice(lastPos);
+            this.push(buf);
+        } else if (!lastPos) {
+            this.push(chunk);
+        }
+        done();
+    }
+}
+
+module.exports = LeWindows;
diff --git a/node_modules/nodemailer/lib/sendmail-transport/le-windows.js b/node_modules/nodemailer/lib/sendmail-transport/le-windows.js
new file mode 100644
index 0000000..873875d
--- /dev/null
+++ b/node_modules/nodemailer/lib/sendmail-transport/le-windows.js
@@ -0,0 +1,55 @@
+'use strict';
+
+const stream = require('stream');
+const Transform = stream.Transform;
+
+/**
+ * Ensures that only <CR><LF> sequences are used for linebreaks
+ *
+ * @param {Object} options Stream options
+ */
+class LeWindows extends Transform {
+
+    constructor(options) {
+        super(options);
+        // init Transform
+        this.options = options || {};
+        this.lastByte = false;
+    }
+
+    /**
+     * Escapes dots
+     */
+    _transform(chunk, encoding, done) {
+        let buf;
+        let lastPos = 0;
+
+        for (let i = 0, len = chunk.length; i < len; i++) {
+            if (chunk[i] === 0x0A) { // \n
+                if (
+                    (i && chunk[i - 1] !== 0x0D) ||
+                    (!i && this.lastByte !== 0x0D)
+                ) {
+                    if (i > lastPos) {
+                        buf = chunk.slice(lastPos, i);
+                        this.push(buf);
+                    }
+                    this.push(new Buffer('\r\n'));
+                    lastPos = i + 1;
+                }
+            }
+        }
+
+        if (lastPos && lastPos < chunk.length) {
+            buf = chunk.slice(lastPos);
+            this.push(buf);
+        } else if (!lastPos) {
+            this.push(chunk);
+        }
+
+        this.lastByte = chunk[chunk.length - 1];
+        done();
+    }
+}
+
+module.exports = LeWindows;
diff --git a/node_modules/nodemailer/lib/ses-transport/index.js b/node_modules/nodemailer/lib/ses-transport/index.js
new file mode 100644
index 0000000..79facee
--- /dev/null
+++ b/node_modules/nodemailer/lib/ses-transport/index.js
@@ -0,0 +1,285 @@
+'use strict';
+
+const EventEmitter = require('events');
+const packageData = require('../../package.json');
+const shared = require('../shared');
+const LeWindows = require('../sendmail-transport/le-windows');
+
+/**
+ * Generates a Transport object for Sendmail
+ *
+ * Possible options can be the following:
+ *
+ *  * **path** optional path to sendmail binary
+ *  * **args** an array of arguments for the sendmail binary
+ *
+ * @constructor
+ * @param {Object} optional config parameter for the AWS Sendmail service
+ */
+class SESTransport extends EventEmitter {
+    constructor(options) {
+        super();
+        options = options || {};
+
+        this.options = options || {};
+        this.ses = this.options.SES;
+
+        this.name = 'SESTransport';
+        this.version = packageData.version;
+
+        this.logger = shared.getLogger(this.options, {
+            component: this.options.component || 'ses-transport'
+        });
+
+        // parallel sending connections
+        this.maxConnections = Number(this.options.maxConnections) || Infinity;
+        this.connections = 0;
+
+        // max messages per second
+        this.sendingRate = Number(this.options.sendingRate) || Infinity;
+        this.sendingRateTTL = null;
+        this.rateInterval = 1000;
+        this.rateMessages = [];
+
+        this.pending = [];
+
+        this.idling = true;
+
+        setImmediate(() => {
+            if (this.idling) {
+                this.emit('idle');
+            }
+        });
+    }
+
+    /**
+     * Schedules a sending of a message
+     *
+     * @param {Object} emailMessage MailComposer object
+     * @param {Function} callback Callback function to run when the sending is completed
+     */
+    send(mail, callback) {
+        if (this.connections >= this.maxConnections) {
+            this.idling = false;
+            return this.pending.push({
+                mail,
+                callback
+            });
+        }
+
+        if (!this._checkSendingRate()) {
+            this.idling = false;
+            return this.pending.push({
+                mail,
+                callback
+            });
+        }
+
+        this._send(mail, (...args) => {
+            setImmediate(() => callback(...args));
+            this._sent();
+        });
+    }
+
+    _checkRatedQueue() {
+        if (this.connections >= this.maxConnections || !this._checkSendingRate()) {
+            return;
+        }
+
+        if (!this.pending.length) {
+            if (!this.idling) {
+                this.idling = true;
+                this.emit('idle');
+            }
+            return;
+        }
+
+        let next = this.pending.shift();
+        this._send(next.mail, (...args) => {
+            setImmediate(() => next.callback(...args));
+            this._sent();
+        });
+    }
+
+    _checkSendingRate() {
+        clearTimeout(this.sendingRateTTL);
+
+        let now = Date.now();
+        let oldest = false;
+        // delete older messages
+        for (let i = this.rateMessages.length - 1; i >= 0; i--) {
+
+            if (this.rateMessages[i].ts >= now - this.rateInterval && (!oldest || this.rateMessages[i].ts < oldest)) {
+                oldest = this.rateMessages[i].ts;
+            }
+
+            if (this.rateMessages[i].ts < now - this.rateInterval && !this.rateMessages[i].pending) {
+                this.rateMessages.splice(i, 1);
+            }
+        }
+
+        if (this.rateMessages.length < this.sendingRate) {
+            return true;
+        }
+
+        let delay = Math.max(oldest + 1001, now + 20);
+        this.sendingRateTTL = setTimeout(() => this._checkRatedQueue(), now - delay);
+        this.sendingRateTTL.unref();
+        return false;
+    }
+
+    _sent() {
+        this.connections--;
+        this._checkRatedQueue();
+    }
+
+    /**
+     * Returns true if there are free slots in the queue
+     */
+    isIdle() {
+        return this.idling;
+    }
+
+    /**
+     * Compiles a mailcomposer message and forwards it to SES
+     *
+     * @param {Object} emailMessage MailComposer object
+     * @param {Function} callback Callback function to run when the sending is completed
+     */
+    _send(mail, callback) {
+        let statObject = {
+            ts: Date.now(),
+            pending: true
+        };
+        this.connections++;
+        this.rateMessages.push(statObject);
+
+        let envelope = mail.data.envelope || mail.message.getEnvelope();
+        let messageId = mail.message.messageId();
+
+        let recipients = [].concat(envelope.to || []);
+        if (recipients.length > 3) {
+            recipients.push('...and ' + recipients.splice(2).length + ' more');
+        }
+        this.logger.info({
+            tnx: 'send',
+            messageId
+        }, 'Sending message %s to <%s>', messageId, recipients.join(', '));
+
+        let getRawMessage = next => {
+
+            // do not use Message-ID and Date in DKIM signature
+            if (!mail.data._dkim) {
+                mail.data._dkim = {};
+            }
+            if (mail.data._dkim.skipFields && typeof mail.data._dkim.skipFields === 'string') {
+                mail.data._dkim.skipFields += ':date:message-id';
+            } else {
+                mail.data._dkim.skipFields = 'date:message-id';
+            }
+
+            let sourceStream = mail.message.createReadStream();
+            let stream = sourceStream.pipe(new LeWindows());
+            let chunks = [];
+            let chunklen = 0;
+
+            stream.on('readable', () => {
+                let chunk;
+                while ((chunk = stream.read()) !== null) {
+                    chunks.push(chunk);
+                    chunklen += chunk.length;
+                }
+            });
+
+            sourceStream.once('error', err => stream.emit('error', err));
+
+            stream.once('error', err => {
+                next(err);
+            });
+
+            stream.once('end', () => next(null, Buffer.concat(chunks, chunklen)));
+        };
+
+        setImmediate(() => getRawMessage((err, raw) => {
+            if (err) {
+                this.logger.error({
+                    err,
+                    tnx: 'send',
+                    messageId
+                }, 'Failed creating message for %s. %s', messageId, err.message);
+                statObject.pending = false;
+                return callback(err);
+            }
+
+            let sesMessage = {
+                RawMessage: { // required
+                    Data: raw // required
+                },
+                Source: envelope.from,
+                Destinations: envelope.to
+            };
+
+            Object.keys(mail.data.ses || {}).forEach(key => {
+                sesMessage[key] = mail.data.ses[key];
+            });
+
+            this.ses.sendRawEmail(sesMessage, (err, data) => {
+                if (err) {
+                    this.logger.error({
+                        err,
+                        tnx: 'send'
+                    }, 'Send error for %s: %s', messageId, err.message);
+                    statObject.pending = false;
+                    return callback(err);
+                }
+
+                let region = this.ses.config && this.ses.config.region || 'us-east-1';
+                if (region === 'us-east-1') {
+                    region = 'email';
+                }
+
+                statObject.pending = false;
+                callback(null, {
+                    envelope: {
+                        from: envelope.from,
+                        to: envelope.to
+                    },
+                    messageId: '<' + data.MessageId + (!/@/.test(data.MessageId) ? '@' + region + '.amazonses.com' : '') + '>',
+                    response: data.MessageId
+                });
+            });
+        }));
+    }
+
+    /**
+     * Verifies SES configuration
+     *
+     * @param {Function} callback Callback function
+     */
+    verify(callback) {
+        let promise;
+
+        if (!callback && typeof Promise === 'function') {
+            promise = new Promise((resolve, reject) => {
+                callback = shared.callbackPromise(resolve, reject);
+            });
+        }
+
+        this.ses.sendRawEmail({
+            RawMessage: { // required
+                Data: 'From: invalid@invalid\r\nTo: invalid@invalid\r\n Subject: Invalid\r\n\r\nInvalid'
+            },
+            Source: 'invalid@invalid',
+            Destinations: ['invalid@invalid']
+        }, err => {
+            if (err && err.code !== 'InvalidParameterValue') {
+                return callback(err);
+            }
+            return callback(null, true);
+        });
+
+        return promise;
+    }
+}
+
+module.exports = SESTransport;
diff --git a/node_modules/nodemailer/lib/shared/index.js b/node_modules/nodemailer/lib/shared/index.js
new file mode 100644
index 0000000..fdc30d9
--- /dev/null
+++ b/node_modules/nodemailer/lib/shared/index.js
@@ -0,0 +1,381 @@
+'use strict';
+
+const urllib = require('url');
+const util = require('util');
+const fs = require('fs');
+const fetch = require('../fetch');
+
+/**
+ * Parses connection url to a structured configuration object
+ *
+ * @param {String} str Connection url
+ * @return {Object} Configuration object
+ */
+module.exports.parseConnectionUrl = str => {
+    str = str || '';
+    let options = {};
+
+    [urllib.parse(str, true)].forEach(url => {
+        let auth;
+
+        switch (url.protocol) {
+            case 'smtp:':
+                options.secure = false;
+                break;
+            case 'smtps:':
+                options.secure = true;
+                break;
+            case 'direct:':
+                options.direct = true;
+                break;
+        }
+
+        if (!isNaN(url.port) && Number(url.port)) {
+            options.port = Number(url.port);
+        }
+
+        if (url.hostname) {
+            options.host = url.hostname;
+        }
+
+        if (url.auth) {
+            auth = url.auth.split(':');
+
+            if (!options.auth) {
+                options.auth = {};
+            }
+
+            options.auth.user = auth.shift();
+            options.auth.pass = auth.join(':');
+        }
+
+        Object.keys(url.query || {}).forEach(key => {
+            let obj = options;
+            let lKey = key;
+            let value = url.query[key];
+
+            if (!isNaN(value)) {
+                value = Number(value);
+            }
+
+            switch (value) {
+                case 'true':
+                    value = true;
+                    break;
+                case 'false':
+                    value = false;
+                    break;
+            }
+
+            // tls is nested object
+            if (key.indexOf('tls.') === 0) {
+                lKey = key.substr(4);
+                if (!options.tls) {
+                    options.tls = {};
+                }
+                obj = options.tls;
+            } else if (key.indexOf('.') >= 0) {
+                // ignore nested properties besides tls
+                return;
+            }
+
+            if (!(lKey in obj)) {
+                obj[lKey] = value;
+            }
+        });
+    });
+
+    return options;
+};
+
+module.exports._logFunc = (logger, level, defaults, data, message, ...args) => {
+    let entry = {};
+
+    Object.keys(defaults || {}).forEach(key => {
+        if (key !== 'level') {
+            entry[key] = defaults[key];
+        }
+    });
+
+    Object.keys(data || {}).forEach(key => {
+        if (key !== 'level') {
+            entry[key] = data[key];
+        }
+    });
+
+    logger[level](entry, message, ...args);
+};
+
+/**
+ * Returns a bunyan-compatible logger interface. Uses either provided logger or
+ * creates a default console logger
+ *
+ * @param {Object} [options] Options object that might include 'logger' value
+ * @return {Object} bunyan compatible logger
+ */
+module.exports.getLogger = (options, defaults) => {
+    options = options || {};
+
+    let response = {};
+    let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
+
+    if (!options.logger) {
+        // use vanity logger
+        levels.forEach(level => {
+            response[level] = () => false;
+        });
+        return response;
+    }
+
+    let logger = options.logger;
+
+    if (options.logger === true) {
+        // create console logger
+        logger = createDefaultLogger(levels);
+    }
+
+    levels.forEach(level => {
+        response[level] = (data, message, ...args) => {
+            module.exports._logFunc(logger, level, defaults, data, message, ...args);
+        };
+    });
+
+    return response;
+};
+
+/**
+ * Wrapper for creating a callback than either resolves or rejects a promise
+ * based on input
+ *
+ * @param {Function} resolve Function to run if callback is called
+ * @param {Function} reject Function to run if callback ends with an error
+ */
+module.exports.callbackPromise = (resolve, reject) => function () {
+    let args = Array.from(arguments);
+    let err = args.shift();
+    if (err) {
+        reject(err);
+    } else {
+        resolve(...args);
+    }
+};
+
+/**
+ * Resolves a String or a Buffer value for content value. Useful if the value
+ * is a Stream or a file or an URL. If the value is a Stream, overwrites
+ * the stream object with the resolved value (you can't stream a value twice).
+ *
+ * This is useful when you want to create a plugin that needs a content value,
+ * for example the `html` or `text` value as a String or a Buffer but not as
+ * a file path or an URL.
+ *
+ * @param {Object} data An object or an Array you want to resolve an element for
+ * @param {String|Number} key Property name or an Array index
+ * @param {Function} callback Callback function with (err, value)
+ */
+module.exports.resolveContent = (data, key, callback) => {
+    let promise;
+
+    if (!callback && typeof Promise === 'function') {
+        promise = new Promise((resolve, reject) => {
+            callback = module.exports.callbackPromise(resolve, reject);
+        });
+    }
+
+    let content = data && data[key] && data[key].content || data[key];
+    let contentStream;
+    let encoding = (typeof data[key] === 'object' && data[key].encoding || 'utf8')
+        .toString()
+        .toLowerCase()
+        .replace(/[-_\s]/g, '');
+
+    if (!content) {
+        return callback(null, content);
+    }
+
+    if (typeof content === 'object') {
+        if (typeof content.pipe === 'function') {
+            return resolveStream(content, (err, value) => {
+                if (err) {
+                    return callback(err);
+                }
+                // we can't stream twice the same content, so we need
+                // to replace the stream object with the streaming result
+                data[key] = value;
+                callback(null, value);
+            });
+        } else if (/^https?:\/\//i.test(content.path || content.href)) {
+            contentStream = fetch(content.path || content.href);
+            return resolveStream(contentStream, callback);
+        } else if (/^data:/i.test(content.path || content.href)) {
+            let parts = (content.path || content.href).match(/^data:((?:[^;]*;)*(?:[^,]*)),(.*)$/i);
+            if (!parts) {
+                return callback(null, new Buffer(0));
+            }
+            return callback(null, /\bbase64$/i.test(parts[1]) ? new Buffer(parts[2], 'base64') : new Buffer(decodeURIComponent(parts[2])));
+        } else if (content.path) {
+            return resolveStream(fs.createReadStream(content.path), callback);
+        }
+    }
+
+    if (typeof data[key].content === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) {
+        content = new Buffer(data[key].content, encoding);
+    }
+
+    // default action, return as is
+    setImmediate(() => callback(null, content));
+
+    return promise;
+};
+
+/**
+ * Copies properties from source objects to target objects
+ */
+module.exports.assign = function ( /* target, ... sources */ ) {
+    let args = Array.from(arguments);
+    let target = args.shift() || {};
+
+    args.forEach(source => {
+        Object.keys(source || {}).forEach(key => {
+            if (['tls', 'auth'].includes(key) && source[key] && typeof source[key] === 'object') {
+                // tls and auth are special keys that need to be enumerated separately
+                // other objects are passed as is
+                if (!target[key]) {
+                    // ensure that target has this key
+                    target[key] = {};
+                }
+                Object.keys(source[key]).forEach(subKey => {
+                    target[key][subKey] = source[key][subKey];
+                });
+            } else {
+                target[key] = source[key];
+            }
+        });
+    });
+    return target;
+};
+
+module.exports.encodeXText = str => {
+    // ! 0x21
+    // + 0x2B
+    // = 0x3D
+    // ~ 0x7E
+    if (!/[^\x21-\x2A\x2C-\x3C\x3E-\x7E]/.test(str)) {
+        return str;
+    }
+    let buf = Buffer.from(str);
+    let result = '';
+    for (let i = 0, len = buf.length; i < len; i++) {
+        let c = buf[i];
+        if (c < 0x21 || c > 0x7e || c === 0x2b || c === 0x3d) {
+            result += '+' + (c < 0x10 ? '0' : '') + c.toString(16).toUpperCase();
+        } else {
+            result += String.fromCharCode(c);
+        }
+    }
+    return result;
+};
+
+
+/**
+ * Streams a stream value into a Buffer
+ *
+ * @param {Object} stream Readable stream
+ * @param {Function} callback Callback function with (err, value)
+ */
+function resolveStream(stream, callback) {
+    let responded = false;
+    let chunks = [];
+    let chunklen = 0;
+
+    stream.on('error', err => {
+        if (responded) {
+            return;
+        }
+
+        responded = true;
+        callback(err);
+    });
+
+    stream.on('readable', () => {
+        let chunk;
+        while ((chunk = stream.read()) !== null) {
+            chunks.push(chunk);
+            chunklen += chunk.length;
+        }
+    });
+
+    stream.on('end', () => {
+        if (responded) {
+            return;
+        }
+        responded = true;
+
+        let value;
+
+        try {
+            value = Buffer.concat(chunks, chunklen);
+        } catch (E) {
+            return callback(E);
+        }
+        callback(null, value);
+    });
+}
+
+/**
+ * Generates a bunyan-like logger that prints to console
+ *
+ * @returns {Object} Bunyan logger instance
+ */
+function createDefaultLogger(levels) {
+
+    let levelMaxLen = 0;
+    let levelNames = new Map();
+    levels.forEach(level => {
+        if (level.length > levelMaxLen) {
+            levelMaxLen = level.length;
+        }
+    });
+
+    levels.forEach(level => {
+        let levelName = level.toUpperCase();
+        if (levelName.length < levelMaxLen) {
+            levelName += ' '.repeat(levelMaxLen - levelName.length);
+        }
+        levelNames.set(level, levelName);
+    });
+
+    let print = (level, entry, message, ...args) => {
+        let prefix = '';
+        if (entry) {
+            if (entry.tnx === 'server') {
+                prefix = 'S: ';
+            } else if (entry.tnx === 'client') {
+                prefix = 'C: ';
+            }
+
+            if (entry.sid) {
+                prefix = '[' + entry.sid + '] ' + prefix;
+            }
+
+            if (entry.cid) {
+                prefix = '[#' + entry.cid + '] ' + prefix;
+            }
+        }
+
+        message = util.format(message, ...args);
+        message.split(/\r?\n/).forEach(line => {
+            console.log('[%s] %s %s', // eslint-disable-line no-console
+                new Date().toISOString().substr(0, 19).replace(/T/, ' '),
+                levelNames.get(level),
+                prefix + line);
+        });
+    };
+
+    let logger = {};
+    levels.forEach(level => {
+        logger[level] = print.bind(null, level);
+    });
+
+    return logger;
+}
diff --git a/node_modules/nodemailer/lib/smtp-connection/data-stream.js b/node_modules/nodemailer/lib/smtp-connection/data-stream.js
new file mode 100644
index 0000000..b690daa
--- /dev/null
+++ b/node_modules/nodemailer/lib/smtp-connection/data-stream.js
@@ -0,0 +1,113 @@
+'use strict';
+
+const stream = require('stream');
+const Transform = stream.Transform;
+
+/**
+ * Escapes dots in the beginning of lines. Ends the stream with <CR><LF>.<CR><LF>
+ * Also makes sure that only <CR><LF> sequences are used for linebreaks
+ *
+ * @param {Object} options Stream options
+ */
+class DataStream extends Transform {
+
+    constructor(options) {
+        super(options);
+        // init Transform
+        this.options = options || {};
+        this._curLine = '';
+
+        this.inByteCount = 0;
+        this.outByteCount = 0;
+        this.lastByte = false;
+
+    }
+
+    /**
+     * Escapes dots
+     */
+    _transform(chunk, encoding, done) {
+        let chunks = [];
+        let chunklen = 0;
+        let i, len, lastPos = 0;
+        let buf;
+
+        if (!chunk || !chunk.length) {
+            return done();
+        }
+
+        if (typeof chunk === 'string') {
+            chunk = new Buffer(chunk);
+        }
+
+        this.inByteCount += chunk.length;
+
+        for (i = 0, len = chunk.length; i < len; i++) {
+            if (chunk[i] === 0x2E) { // .
+                if (
+                    (i && chunk[i - 1] === 0x0A) ||
+                    (!i && (!this.lastByte || this.lastByte === 0x0A))
+                ) {
+                    buf = chunk.slice(lastPos, i + 1);
+                    chunks.push(buf);
+                    chunks.push(new Buffer('.'));
+                    chunklen += buf.length + 1;
+                    lastPos = i + 1;
+                }
+            } else if (chunk[i] === 0x0A) { // .
+                if (
+                    (i && chunk[i - 1] !== 0x0D) ||
+                    (!i && this.lastByte !== 0x0D)
+                ) {
+                    if (i > lastPos) {
+                        buf = chunk.slice(lastPos, i);
+                        chunks.push(buf);
+                        chunklen += buf.length + 2;
+                    } else {
+                        chunklen += 2;
+                    }
+                    chunks.push(new Buffer('\r\n'));
+                    lastPos = i + 1;
+                }
+            }
+        }
+
+        if (chunklen) {
+            // add last piece
+            if (lastPos < chunk.length) {
+                buf = chunk.slice(lastPos);
+                chunks.push(buf);
+                chunklen += buf.length;
+            }
+
+            this.outByteCount += chunklen;
+            this.push(Buffer.concat(chunks, chunklen));
+        } else {
+            this.outByteCount += chunk.length;
+            this.push(chunk);
+        }
+
+        this.lastByte = chunk[chunk.length - 1];
+        done();
+    }
+
+    /**
+     * Finalizes the stream with a dot on a single line
+     */
+    _flush(done) {
+        let buf;
+        if (this.lastByte === 0x0A) {
+            buf = new Buffer('.\r\n');
+        } else if (this.lastByte === 0x0D) {
+            buf = new Buffer('\n.\r\n');
+        } else {
+            buf = new Buffer('\r\n.\r\n');
+        }
+        this.outByteCount += buf.length;
+        this.push(buf);
+        done();
+    }
+
+}
+
+module.exports = DataStream;
diff --git a/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js b/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js
new file mode 100644
index 0000000..3d89aad
--- /dev/null
+++ b/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js
@@ -0,0 +1,126 @@
+'use strict';
+
+/**
+ * Minimal HTTP/S proxy client
+ */
+
+const net = require('net');
+const tls = require('tls');
+const urllib = require('url');
+
+/**
+ * Establishes proxied connection to destinationPort
+ *
+ * httpProxyClient("http://localhost:3128/", 80, "google.com", function(err, socket){
+ *     socket.write("GET / HTTP/1.0\r\n\r\n");
+ * });
+ *
+ * @param {String} proxyUrl proxy configuration, etg "http://proxy.host:3128/"
+ * @param {Number} destinationPort Port to open in destination host
+ * @param {String} destinationHost Destination hostname
+ * @param {Function} callback Callback to run with the rocket object once connection is established
+ */
+function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) {
+    let proxy = urllib.parse(proxyUrl);
+
+    // create a socket connection to the proxy server
+    let options;
+    let connect;
+    let socket;
+
+    options = {
+        host: proxy.hostname,
+        port: Number(proxy.port) ? Number(proxy.port) : (proxy.protocol === 'https:' ? 443 : 80)
+    };
+
+    if (proxy.protocol === 'https:') {
+        // we can use untrusted proxies as long as we verify actual SMTP certificates
+        options.rejectUnauthorized = false;
+        connect = tls.connect.bind(tls);
+    } else {
+        connect = net.connect.bind(net);
+    }
+
+    // Error harness for initial connection. Once connection is established, the responsibility
+    // to handle errors is passed to whoever uses this socket
+    let finished = false;
+    let tempSocketErr = function (err) {
+        if (finished) {
+            return;
+        }
+        finished = true;
+        try {
+            socket.destroy();
+        } catch (E) {
+            // ignore
+        }
+        callback(err);
+    };
+
+    socket = connect(options, () => {
+        if (finished) {
+            return;
+        }
+
+        let reqHeaders = {
+            Host: destinationHost + ':' + destinationPort,
+            Connection: 'close'
+        };
+        if (proxy.auth) {
+            reqHeaders['Proxy-Authorization'] = 'Basic ' + new Buffer(proxy.auth).toString('base64');
+        }
+
+        socket.write(
+            // HTTP method
+            'CONNECT ' + destinationHost + ':' + destinationPort + ' HTTP/1.1\r\n' +
+
+            // HTTP request headers
+            Object.keys(reqHeaders).map(key => key + ': ' + reqHeaders[key]).join('\r\n') +
+
+            // End request
+            '\r\n\r\n');
+
+        let headers = '';
+        let onSocketData = chunk => {
+            let match;
+            let remainder;
+
+            if (finished) {
+                return;
+            }
+
+            headers += chunk.toString('binary');
+            if ((match = headers.match(/\r\n\r\n/))) {
+                socket.removeListener('data', onSocketData);
+
+                remainder = headers.substr(match.index + match[0].length);
+                headers = headers.substr(0, match.index);
+                if (remainder) {
+                    socket.unshift(new Buffer(remainder, 'binary'));
+                }
+
+                // proxy connection is now established
+                finished = true;
+
+                // check response code
+                match = headers.match(/^HTTP\/\d+\.\d+ (\d+)/i);
+                if (!match || (match[1] || '').charAt(0) !== '2') {
+                    try {
+                        socket.destroy();
+                    } catch (E) {
+                        // ignore
+                    }
+                    return callback(new Error('Invalid response from proxy' + (match && ': ' + match[1] || '')));
+                }
+
+                socket.removeListener('error', tempSocketErr);
+                return callback(null, socket);
+            }
+        };
+        socket.on('data', onSocketData);
+    });
+
+    socket.once('error', tempSocketErr);
+}
+
+module.exports = httpProxyClient;
diff --git a/node_modules/nodemailer/lib/smtp-connection/index.js b/node_modules/nodemailer/lib/smtp-connection/index.js
new file mode 100644
index 0000000..119436a
--- /dev/null
+++ b/node_modules/nodemailer/lib/smtp-connection/index.js
@@ -0,0 +1,1456 @@
+'use strict';
+
+const packageInfo = require('../../package.json');
+const EventEmitter = require('events').EventEmitter;
+const net = require('net');
+const tls = require('tls');
+const os = require('os');
+const crypto = require('crypto');
+const DataStream = require('./data-stream');
+const PassThrough = require('stream').PassThrough;
+const shared = require('../shared');
+
+// default timeout values in ms
+const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection to be established
+const SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity before disconnecting the client
+const GREETING_TIMEOUT = 30 * 1000; // how much to wait after connection is established but SMTP greeting is not receieved
+
+/**
+ * Generates a SMTP connection object
+ *
+ * Optional options object takes the following possible properties:
+ *
+ *  * **port** - is the port to connect to (defaults to 587 or 465)
+ *  * **host** - is the hostname or IP address to connect to (defaults to 'localhost')
+ *  * **secure** - use SSL
+ *  * **ignoreTLS** - ignore server support for STARTTLS
+ *  * **requireTLS** - forces the client to use STARTTLS
+ *  * **name** - the name of the client server
+ *  * **localAddress** - outbound address to bind to (see: http://nodejs.org/api/net.html#net_net_connect_options_connectionlistener)
+ *  * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 10000)
+ *  * **connectionTimeout** - how many milliseconds to wait for the connection to establish
+ *  * **socketTimeout** - Time of inactivity until the connection is closed (defaults to 1 hour)
+ *  * **lmtp** - if true, uses LMTP instead of SMTP protocol
+ *  * **logger** - bunyan compatible logger interface
+ *  * **debug** - if true pass SMTP traffic to the logger
+ *  * **tls** - options for createCredentials
+ *  * **socket** - existing socket to use instead of creating a new one (see: http://nodejs.org/api/net.html#net_class_net_socket)
+ *  * **secured** - boolean indicates that the provided socket has already been upgraded to tls
+ *
+ * @constructor
+ * @namespace SMTP Client module
+ * @param {Object} [options] Option properties
+ */
+class SMTPConnection extends EventEmitter {
+    constructor(options) {
+        super(options);
+
+        this.id = crypto.randomBytes(8).toString('base64').replace(/\W/g, '');
+        this.stage = 'init';
+
+        this.options = options || {};
+
+        this.secureConnection = !!this.options.secure;
+        this.alreadySecured = !!this.options.secured;
+
+        this.port = this.options.port || (this.secureConnection ? 465 : 587);
+        this.host = this.options.host || 'localhost';
+
+        if (typeof this.options.secure === 'undefined' && this.port === 465) {
+            // if secure option is not set but port is 465, then default to secure
+            this.secureConnection = true;
+        }
+
+        this.name = this.options.name || this._getHostname();
+
+        this.logger = shared.getLogger(this.options, {
+            component: this.options.component || 'smtp-connection',
+            sid: this.id
+        });
+
+        /**
+         * Expose version nr, just for the reference
+         * @type {String}
+         */
+        this.version = packageInfo.version;
+
+        /**
+         * If true, then the user is authenticated
+         * @type {Boolean}
+         */
+        this.authenticated = false;
+
+        /**
+         * If set to true, this instance is no longer active
+         * @private
+         */
+        this.destroyed = false;
+
+        /**
+         * Defines if the current connection is secure or not. If not,
+         * STARTTLS can be used if available
+         * @private
+         */
+        this.secure = !!this.secureConnection;
+
+        /**
+         * Store incomplete messages coming from the server
+         * @private
+         */
+        this._remainder = '';
+
+        /**
+         * Unprocessed responses from the server
+         * @type {Array}
+         */
+        this._responseQueue = [];
+
+        this.lastServerResponse = false;
+
+        /**
+         * The socket connecting to the server
+         * @publick
+         */
+        this._socket = false;
+
+        /**
+         * Lists supported auth mechanisms
+         * @private
+         */
+        this._supportedAuth = [];
+
+        /**
+         * Includes current envelope (from, to)
+         * @private
+         */
+        this._envelope = false;
+
+        /**
+         * Lists supported extensions
+         * @private
+         */
+        this._supportedExtensions = [];
+
+        /**
+         * Defines the maximum allowed size for a single message
+         * @private
+         */
+        this._maxAllowedSize = 0;
+
+        /**
+         * Function queue to run if a data chunk comes from the server
+         * @private
+         */
+        this._responseActions = [];
+        this._recipientQueue = [];
+
+        /**
+         * Timeout variable for waiting the greeting
+         * @private
+         */
+        this._greetingTimeout = false;
+
+        /**
+         * Timeout variable for waiting the connection to start
+         * @private
+         */
+        this._connectionTimeout = false;
+
+        /**
+         * If the socket is deemed already closed
+         * @private
+         */
+        this._destroyed = false;
+
+        /**
+         * If the socket is already being closed
+         * @private
+         */
+        this._closing = false;
+    }
+
+    /**
+     * Creates a connection to a SMTP server and sets up connection
+     * listener
+     */
+    connect(connectCallback) {
+        if (typeof connectCallback === 'function') {
+            this.once('connect', () => {
+                this.logger.debug({
+                    tnx: 'smtp'
+                }, 'SMTP handshake finished');
+                connectCallback();
+            });
+        }
+
+        let opts = {
+            port: this.port,
+            host: this.host
+        };
+
+        if (this.options.localAddress) {
+            opts.localAddress = this.options.localAddress;
+        }
+
+        if (this.options.connection) {
+            // connection is already opened
+            this._socket = this.options.connection;
+            if (this.secureConnection && !this.alreadySecured) {
+                setImmediate(() => this._upgradeConnection(err => {
+                    if (err) {
+                        this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'CONN');
+                        return;
+                    }
+                    this._onConnect();
+                }));
+            } else {
+                setImmediate(() => this._onConnect());
+            }
+        } else if (this.options.socket) {
+            // socket object is set up but not yet connected
+            this._socket = this.options.socket;
+            try {
+                this._socket.connect(this.port, this.host, () => {
+                    this._socket.setKeepAlive(true);
+                    this._onConnect();
+                });
+            } catch (E) {
+                return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
+            }
+        } else if (this.secureConnection) {
+            // connect using tls
+            if (this.options.tls) {
+                Object.keys(this.options.tls).forEach(key => {
+                    opts[key] = this.options.tls[key];
+                });
+            }
+            try {
+                this._socket = tls.connect(this.port, this.host, opts, () => {
+                    this._socket.setKeepAlive(true);
+                    this._onConnect();
+                });
+            } catch (E) {
+                return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
+            }
+        } else {
+            // connect using plaintext
+            try {
+                this._socket = net.connect(opts, () => {
+                    this._socket.setKeepAlive(true);
+                    this._onConnect();
+                });
+            } catch (E) {
+                return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
+            }
+        }
+
+        this._connectionTimeout = setTimeout(() => {
+            this._onError('Connection timeout', 'ETIMEDOUT', false, 'CONN');
+        }, this.options.connectionTimeout || CONNECTION_TIMEOUT);
+
+        this._socket.on('error', err => {
+            this._onError(err, 'ECONNECTION', false, 'CONN');
+        });
+    }
+
+    /**
+     * Sends QUIT
+     */
+    quit() {
+        this._sendCommand('QUIT');
+        this._responseActions.push(this.close);
+    }
+
+    /**
+     * Closes the connection to the server
+     */
+    close() {
+        clearTimeout(this._connectionTimeout);
+        clearTimeout(this._greetingTimeout);
+        this._responseActions = [];
+
+        // allow to run this function only once
+        if (this._closing) {
+            return;
+        }
+        this._closing = true;
+
+        let closeMethod = 'end';
+
+        if (this.stage === 'init') {
+            // Close the socket immediately when connection timed out
+            closeMethod = 'destroy';
+        }
+
+        this.logger.debug({
+            tnx: 'smtp'
+        }, 'Closing connection to the server using "%s"', closeMethod);
+
+        let socket = this._socket && this._socket.socket || this._socket;
+
+        if (socket && !socket.destroyed) {
+            try {
+                this._socket[closeMethod]();
+            } catch (E) {
+                // just ignore
+            }
+        }
+
+        this._destroy();
+    }
+
+    /**
+     * Authenticate user
+     */
+    login(authData, callback) {
+        this._auth = authData || {};
+
+        // Select SASL authentication method
+        this._authMethod = (this._auth.method || '').toString().trim().toUpperCase() || false;
+        if (!this._authMethod && this._auth.oauth2 && !this._auth.credentials) {
+            this._authMethod = 'XOAUTH2';
+        } else if (!this._authMethod || (this._authMethod === 'XOAUTH2' && !this._auth.oauth2)) {
+            // use first supported
+            this._authMethod = (this._supportedAuth[0] || 'PLAIN').toUpperCase().trim();
+        }
+
+        if (this._authMethod !== 'XOAUTH2' && !this._auth.credentials) {
+            if (this._auth.user && this._auth.pass) {
+                this._auth.credentials = {
+                    user: this._auth.user,
+                    pass: this._auth.pass
+                };
+            } else {
+                return callback(this._formatError('Missing credentials for "' + this._authMethod + '"', 'EAUTH', false, 'API'));
+            }
+        }
+
+        switch (this._authMethod) {
+            case 'XOAUTH2':
+                this._handleXOauth2Token(false, callback);
+                return;
+            case 'LOGIN':
+                this._responseActions.push(str => {
+                    this._actionAUTH_LOGIN_USER(str, callback);
+                });
+                this._sendCommand('AUTH LOGIN');
+                return;
+            case 'PLAIN':
+                this._responseActions.push(str => {
+                    this._actionAUTHComplete(str, callback);
+                });
+                this._sendCommand('AUTH PLAIN ' + new Buffer(
+                    //this._auth.user+'\u0000'+
+                    '\u0000' + // skip authorization identity as it causes problems with some servers
+                    this._auth.credentials.user + '\u0000' +
+                    this._auth.credentials.pass, 'utf-8').toString('base64'));
+                return;
+            case 'CRAM-MD5':
+                this._responseActions.push(str => {
+                    this._actionAUTH_CRAM_MD5(str, callback);
+                });
+                this._sendCommand('AUTH CRAM-MD5');
+                return;
+        }
+
+        return callback(this._formatError('Unknown authentication method "' + this._authMethod + '"', 'EAUTH', false, 'API'));
+    }
+
+    /**
+     * Sends a message
+     *
+     * @param {Object} envelope Envelope object, {from: addr, to: [addr]}
+     * @param {Object} message String, Buffer or a Stream
+     * @param {Function} callback Callback to return once sending is completed
+     */
+    send(envelope, message, done) {
+        if (!message) {
+            return done(this._formatError('Empty message', 'EMESSAGE', false, 'API'));
+        }
+
+        // reject larger messages than allowed
+        if (this._maxAllowedSize && envelope.size > this._maxAllowedSize) {
+            return setImmediate(() => {
+                done(this._formatError('Message size larger than allowed ' + this._maxAllowedSize, 'EMESSAGE', false, 'MAIL FROM'));
+            });
+        }
+
+        // ensure that callback is only called once
+        let returned = false;
+        let callback = function () {
+            if (returned) {
+                return;
+            }
+            returned = true;
+
+            done(...arguments);
+        };
+
+        if (typeof message.on === 'function') {
+            message.on('error', err => callback(this._formatError(err, 'ESTREAM', false, 'API')));
+        }
+
+        this._setEnvelope(envelope, (err, info) => {
+            if (err) {
+                return callback(err);
+            }
+            let stream = this._createSendStream((err, str) => {
+                if (err) {
+                    return callback(err);
+                }
+                info.response = str;
+                return callback(null, info);
+            });
+            if (typeof message.pipe === 'function') {
+                message.pipe(stream);
+            } else {
+                stream.write(message);
+                stream.end();
+            }
+
+        });
+    }
+
+    /**
+     * Resets connection state
+     *
+     * @param {Function} callback Callback to return once connection is reset
+     */
+    reset(callback) {
+        this._sendCommand('RSET');
+        this._responseActions.push(str => {
+            if (str.charAt(0) !== '2') {
+                return callback(this._formatError('Could not reset session state:\n' + str, 'EPROTOCOL', str, 'RSET'));
+            }
+            this._envelope = false;
+            return callback(null, true);
+        });
+    }
+
+    /**
+     * Connection listener that is run when the connection to
+     * the server is opened
+     *
+     * @event
+     */
+    _onConnect() {
+        clearTimeout(this._connectionTimeout);
+
+        this.logger.info({
+            tnx: 'network',
+            localAddress: this._socket.localAddress,
+            localPort: this._socket.localPort,
+            remoteAddress: this._socket.remoteAddress,
+            remotePort: this._socket.remotePort
+        }, '%s established to %s:%s', this.secure ? 'Secure connection' : 'Connection', this._socket.remoteAddress, this._socket.remotePort);
+
+        if (this._destroyed) {
+            // Connection was established after we already had canceled it
+            this.close();
+            return;
+        }
+
+        this.stage = 'connected';
+
+        // clear existing listeners for the socket
+        this._socket.removeAllListeners('data');
+        this._socket.removeAllListeners('timeout');
+        this._socket.removeAllListeners('close');
+        this._socket.removeAllListeners('end');
+
+        this._socket.on('data', chunk => this._onData(chunk));
+        this._socket.once('close', errored => this._onClose(errored));
+        this._socket.once('end', () => this._onEnd());
+
+        this._socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT);
+        this._socket.on('timeout', () => this._onTimeout());
+
+        this._greetingTimeout = setTimeout(() => {
+            // if still waiting for greeting, give up
+            if (this._socket && !this._destroyed && this._responseActions[0] === this._actionGreeting) {
+                this._onError('Greeting never received', 'ETIMEDOUT', false, 'CONN');
+            }
+        }, this.options.greetingTimeout || GREETING_TIMEOUT);
+
+        this._responseActions.push(this._actionGreeting);
+
+        // we have a 'data' listener set up so resume socket if it was paused
+        this._socket.resume();
+    }
+
+    /**
+     * 'data' listener for data coming from the server
+     *
+     * @event
+     * @param {Buffer} chunk Data chunk coming from the server
+     */
+    _onData(chunk) {
+        if (this._destroyed || !chunk || !chunk.length) {
+            return;
+        }
+
+        let data = (chunk || '').toString('binary');
+        let lines = (this._remainder + data).split(/\r?\n/);
+        let lastline;
+
+        this._remainder = lines.pop();
+
+        for (let i = 0, len = lines.length; i < len; i++) {
+            if (this._responseQueue.length) {
+                lastline = this._responseQueue[this._responseQueue.length - 1];
+                if (/^\d+\-/.test(lastline.split('\n').pop())) {
+                    this._responseQueue[this._responseQueue.length - 1] += '\n' + lines[i];
+                    continue;
+                }
+            }
+            this._responseQueue.push(lines[i]);
+        }
+
+        this._processResponse();
+    }
+
+    /**
+     * 'error' listener for the socket
+     *
+     * @event
+     * @param {Error} err Error object
+     * @param {String} type Error name
+     */
+    _onError(err, type, data, command) {
+        clearTimeout(this._connectionTimeout);
+        clearTimeout(this._greetingTimeout);
+
+        if (this._destroyed) {
+            // just ignore, already closed
+            // this might happen when a socket is canceled because of reached timeout
+            // but the socket timeout error itself receives only after
+            return;
+        }
+
+        err = this._formatError(err, type, data, command);
+
+        let entry = {
+            err
+        };
+        if (type) {
+            entry.errorType = type;
+        }
+        if (data) {
+            entry.errorData = data;
+        }
+        if (command) {
+            entry.command = command;
+        }
+
+        this.logger.error(data, err.message);
+
+        this.emit('error', err);
+        this.close();
+    }
+
+    _formatError(message, type, response, command) {
+        let err;
+
+        if (/Error\]$/i.test(Object.prototype.toString.call(message))) {
+            err = message;
+        } else {
+            err = new Error(message);
+        }
+
+        if (type && type !== 'Error') {
+            err.code = type;
+        }
+
+        if (response) {
+            err.response = response;
+            err.message += ': ' + response;
+        }
+
+        let responseCode = typeof response === 'string' && Number((response.match(/^\d+/) || [])[0]) || false;
+        if (responseCode) {
+            err.responseCode = responseCode;
+        }
+
+        if (command) {
+            err.command = command;
+        }
+
+        return err;
+    }
+
+    /**
+     * 'close' listener for the socket
+     *
+     * @event
+     */
+    _onClose() {
+        this.logger.info({
+            tnx: 'network'
+        }, 'Connection closed');
+
+        if (this.upgrading && !this._destroyed) {
+            return this._onError(new Error('Connection closed unexpectedly'), 'ETLS', false, 'CONN');
+        } else if (![this._actionGreeting, this.close].includes(this._responseActions[0]) && !this._destroyed) {
+            return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', false, 'CONN');
+        }
+
+        this._destroy();
+    }
+
+    /**
+     * 'end' listener for the socket
+     *
+     * @event
+     */
+    _onEnd() {
+        this._destroy();
+    }
+
+    /**
+     * 'timeout' listener for the socket
+     *
+     * @event
+     */
+    _onTimeout() {
+        return this._onError(new Error('Timeout'), 'ETIMEDOUT', false, 'CONN');
+    }
+
+    /**
+     * Destroys the client, emits 'end'
+     */
+    _destroy() {
+        if (this._destroyed) {
+            return;
+        }
+        this._destroyed = true;
+        this.emit('end');
+    }
+
+    /**
+     * Upgrades the connection to TLS
+     *
+     * @param {Function} callback Callback function to run when the connection
+     *        has been secured
+     */
+    _upgradeConnection(callback) {
+        // do not remove all listeners or it breaks node v0.10 as there's
+        // apparently a 'finish' event set that would be cleared as well
+
+        // we can safely keep 'error', 'end', 'close' etc. events
+        this._socket.removeAllListeners('data'); // incoming data is going to be gibberish from this point onwards
+        this._socket.removeAllListeners('timeout'); // timeout will be re-set for the new socket object
+
+        let socketPlain = this._socket;
+        let opts = {
+            socket: this._socket,
+            host: this.host
+        };
+
+        Object.keys(this.options.tls || {}).forEach(key => {
+            opts[key] = this.options.tls[key];
+        });
+
+        this.upgrading = true;
+        this._socket = tls.connect(opts, () => {
+            this.secure = true;
+            this.upgrading = false;
+            this._socket.on('data', chunk => this._onData(chunk));
+
+            socketPlain.removeAllListeners('close');
+            socketPlain.removeAllListeners('end');
+
+            return callback(null, true);
+        });
+
+        this._socket.on('error', err => this._onError(err, 'ESOCKET', false, 'CONN'));
+        this._socket.once('close', errored => this._onClose(errored));
+        this._socket.once('end', () => this._onEnd());
+
+        this._socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT); // 10 min.
+        this._socket.on('timeout', () => this._onTimeout());
+
+        // resume in case the socket was paused
+        socketPlain.resume();
+    }
+
+    /**
+     * Processes queued responses from the server
+     *
+     * @param {Boolean} force If true, ignores _processing flag
+     */
+    _processResponse() {
+        if (!this._responseQueue.length) {
+            return false;
+        }
+
+        let str = this.lastServerResponse = (this._responseQueue.shift() || '').toString();
+
+        if (/^\d+\-/.test(str.split('\n').pop())) {
+            // keep waiting for the final part of multiline response
+            return;
+        }
+
+        if (this.options.debug || this.options.transactionLog) {
+            this.logger.debug({
+                tnx: 'server'
+            }, str.replace(/\r?\n$/, ''));
+        }
+
+        if (!str.trim()) { // skip unexpected empty lines
+            setImmediate(() => this._processResponse(true));
+        }
+
+        let action = this._responseActions.shift();
+
+        if (typeof action === 'function') {
+            action.call(this, str);
+            setImmediate(() => this._processResponse(true));
+        } else {
+            return this._onError(new Error('Unexpected Response'), 'EPROTOCOL', str, 'CONN');
+        }
+    }
+
+    /**
+     * Send a command to the server, append \r\n
+     *
+     * @param {String} str String to be sent to the server
+     */
+    _sendCommand(str) {
+        if (this._destroyed) {
+            // Connection already closed, can't send any more data
+            return;
+        }
+
+        if (this._socket.destroyed) {
+            return this.close();
+        }
+
+        if (this.options.debug || this.options.transactionLog) {
+            this.logger.debug({
+                tnx: 'client'
+            }, (str || '').toString().replace(/\r?\n$/, ''));
+        }
+
+        this._socket.write(new Buffer(str + '\r\n', 'utf-8'));
+    }
+
+    /**
+     * Initiates a new message by submitting envelope data, starting with
+     * MAIL FROM: command
+     *
+     * @param {Object} envelope Envelope object in the form of
+     *        {from:'...', to:['...']}
+     *        or
+     *        {from:{address:'...',name:'...'}, to:[address:'...',name:'...']}
+     */
+    _setEnvelope(envelope, callback) {
+        let args = [];
+        let useSmtpUtf8 = false;
+
+        this._envelope = envelope || {};
+        this._envelope.from = (this._envelope.from && this._envelope.from.address || this._envelope.from || '').toString().trim();
+
+        this._envelope.to = [].concat(this._envelope.to || []).map(to => (to && to.address || to || '').toString().trim());
+
+        if (!this._envelope.to.length) {
+            return callback(this._formatError('No recipients defined', 'EENVELOPE', false, 'API'));
+        }
+
+        if (this._envelope.from && /[\r\n<>]/.test(this._envelope.from)) {
+            return callback(this._formatError('Invalid sender ' + JSON.stringify(this._envelope.from), 'EENVELOPE', false, 'API'));
+        }
+
+        // check if the sender address uses only ASCII characters,
+        // otherwise require usage of SMTPUTF8 extension
+        if (/[\x80-\uFFFF]/.test(this._envelope.from)) {
+            useSmtpUtf8 = true;
+        }
+
+        for (let i = 0, len = this._envelope.to.length; i < len; i++) {
+            if (!this._envelope.to[i] || /[\r\n<>]/.test(this._envelope.to[i])) {
+                return callback(this._formatError('Invalid recipient ' + JSON.stringify(this._envelope.to[i]), 'EENVELOPE', false, 'API'));
+            }
+
+            // check if the recipients addresses use only ASCII characters,
+            // otherwise require usage of SMTPUTF8 extension
+            if (/[\x80-\uFFFF]/.test(this._envelope.to[i])) {
+                useSmtpUtf8 = true;
+            }
+        }
+
+        // clone the recipients array for latter manipulation
+        this._envelope.rcptQueue = JSON.parse(JSON.stringify(this._envelope.to || []));
+        this._envelope.rejected = [];
+        this._envelope.rejectedErrors = [];
+        this._envelope.accepted = [];
+
+        if (this._envelope.dsn) {
+            try {
+                this._envelope.dsn = this._setDsnEnvelope(this._envelope.dsn);
+            } catch (err) {
+                return callback(this._formatError('Invalid DSN ' + err.message, 'EENVELOPE', false, 'API'));
+            }
+        }
+
+        this._responseActions.push(str => {
+            this._actionMAIL(str, callback);
+        });
+
+        // If the server supports SMTPUTF8 and the envelope includes an internationalized
+        // email address then append SMTPUTF8 keyword to the MAIL FROM command
+        if (useSmtpUtf8 && this._supportedExtensions.includes('SMTPUTF8')) {
+            args.push('SMTPUTF8');
+            this._usingSmtpUtf8 = true;
+        }
+
+        // If the server supports 8BITMIME and the message might contain non-ascii bytes
+        // then append the 8BITMIME keyword to the MAIL FROM command
+        if (this._envelope.use8BitMime && this._supportedExtensions.includes('8BITMIME')) {
+            args.push('BODY=8BITMIME');
+            this._using8BitMime = true;
+        }
+
+        if (this._envelope.size && this._supportedExtensions.includes('SIZE')) {
+            args.push('SIZE=' + this._envelope.size);
+        }
+
+        // If the server supports DSN and the envelope includes an DSN prop
+        // then append DSN params to the MAIL FROM command
+        if (this._envelope.dsn && this._supportedExtensions.includes('DSN')) {
+            if (this._envelope.dsn.ret) {
+                args.push('RET=' + shared.encodeXText(this._envelope.dsn.ret));
+            }
+            if (this._envelope.dsn.envid) {
+                args.push('ENVID=' + shared.encodeXText(this._envelope.dsn.envid));
+            }
+        }
+
+        this._sendCommand('MAIL FROM:<' + (this._envelope.from) + '>' + (args.length ? ' ' + args.join(' ') : ''));
+    }
+
+    _setDsnEnvelope(params) {
+        let ret = (params.ret || params.return || '').toString().toUpperCase() || null;
+        if (ret) {
+            switch (ret) {
+                case 'HDRS':
+                case 'HEADERS':
+                    ret = 'HDRS';
+                    break;
+                case 'FULL':
+                case 'BODY':
+                    ret = 'full';
+                    break;
+            }
+        }
+
+        if (ret && !['FULL', 'HDRS'].includes(ret)) {
+            throw new Error('ret: ' + JSON.stringify(ret));
+        }
+
+        let envid = (params.envid || params.id || '').toString() || null;
+
+        let notify = params.notify || null;
+        if (notify) {
+            if (typeof notify === 'string') {
+                notify = notify.split(',');
+            }
+            notify = notify.map(n => n.trim().toUpperCase());
+            let validNotify = ['NEVER', 'SUCCESS', 'FAILURE', 'DELAY'];
+            let invaliNotify = notify.filter(n => !validNotify.includes(n));
+            if (invaliNotify.length || (notify.length > 1 && notify.includes('NEVER'))) {
+                throw new Error('notify: ' + JSON.stringify(notify.join(',')));
+            }
+            notify = notify.join(',');
+        }
+
+        let orcpt = (params.orcpt || params.recipient).toString() || null;
+        if (orcpt && orcpt.indexOf(';') < 0) {
+            orcpt = 'rfc822;' + orcpt;
+        }
+
+        return {
+            ret,
+            envid,
+            notify,
+            orcpt
+        };
+    }
+
+    _getDsnRcptToArgs() {
+        let args = [];
+        // If the server supports DSN and the envelope includes an DSN prop
+        // then append DSN params to the RCPT TO command
+        if (this._envelope.dsn && this._supportedExtensions.includes('DSN')) {
+            if (this._envelope.dsn.notify) {
+                args.push('NOTIFY=' + shared.encodeXText(this._envelope.dsn.notify));
+            }
+            if (this._envelope.dsn.orcpt) {
+                args.push('ORCPT=' + shared.encodeXText(this._envelope.dsn.orcpt));
+            }
+        }
+        return (args.length ? ' ' + args.join(' ') : '');
+    }
+
+    _createSendStream(callback) {
+        let dataStream = new DataStream();
+        let logStream;
+
+        if (this.options.lmtp) {
+            this._envelope.accepted.forEach((recipient, i) => {
+                let final = i === this._envelope.accepted.length - 1;
+                this._responseActions.push(str => {
+                    this._actionLMTPStream(recipient, final, str, callback);
+                });
+            });
+        } else {
+            this._responseActions.push(str => {
+                this._actionSMTPStream(str, callback);
+            });
+        }
+
+        dataStream.pipe(this._socket, {
+            end: false
+        });
+
+        if (this.options.debug) {
+            logStream = new PassThrough();
+            logStream.on('readable', () => {
+                let chunk;
+                while ((chunk = logStream.read())) {
+                    this.logger.debug({
+                        tnx: 'message'
+                    }, chunk.toString('binary').replace(/\r?\n$/, ''));
+                }
+            });
+            dataStream.pipe(logStream);
+        }
+
+        dataStream.once('end', () => {
+            this.logger.info({
+                tnx: 'message',
+                inByteCount: dataStream.inByteCount,
+                outByteCount: dataStream.outByteCount
+            }, '<%s bytes encoded mime message (source size %s bytes)>', dataStream.outByteCount, dataStream.inByteCount);
+        });
+
+        return dataStream;
+    }
+
+    /** ACTIONS **/
+
+    /**
+     * Will be run after the connection is created and the server sends
+     * a greeting. If the incoming message starts with 220 initiate
+     * SMTP session by sending EHLO command
+     *
+     * @param {String} str Message from the server
+     */
+    _actionGreeting(str) {
+        clearTimeout(this._greetingTimeout);
+
+        if (str.substr(0, 3) !== '220') {
+            this._onError(new Error('Invalid greeting from server:\n' + str), 'EPROTOCOL', str, 'CONN');
+            return;
+        }
+
+        if (this.options.lmtp) {
+            this._responseActions.push(this._actionLHLO);
+            this._sendCommand('LHLO ' + this.name);
+        } else {
+            this._responseActions.push(this._actionEHLO);
+            this._sendCommand('EHLO ' + this.name);
+        }
+    }
+
+    /**
+     * Handles server response for LHLO command. If it yielded in
+     * error, emit 'error', otherwise treat this as an EHLO response
+     *
+     * @param {String} str Message from the server
+     */
+    _actionLHLO(str) {
+        if (str.charAt(0) !== '2') {
+            this._onError(new Error('Invalid response for LHLO:\n' + str), 'EPROTOCOL', str, 'LHLO');
+            return;
+        }
+
+        this._actionEHLO(str);
+    }
+
+    /**
+     * Handles server response for EHLO command. If it yielded in
+     * error, try HELO instead, otherwise initiate TLS negotiation
+     * if STARTTLS is supported by the server or move into the
+     * authentication phase.
+     *
+     * @param {String} str Message from the server
+     */
+    _actionEHLO(str) {
+        let match;
+
+        if (str.substr(0, 3) === '421') {
+            this._onError(new Error('Server terminates connection:\n' + str), 'ECONNECTION', str, 'EHLO');
+            return;
+        }
+
+        if (str.charAt(0) !== '2') {
+            if (this.options.requireTLS) {
+                this._onError(new Error('EHLO failed but HELO does not support required STARTTLS:\n' + str), 'ECONNECTION', str, 'EHLO');
+                return;
+            }
+
+            // Try HELO instead
+            this._responseActions.push(this._actionHELO);
+            this._sendCommand('HELO ' + this.name);
+            return;
+        }
+
+        // Detect if the server supports STARTTLS
+        if (!this.secure && !this.options.ignoreTLS && (/[ \-]STARTTLS\b/mi.test(str) || this.options.requireTLS)) {
+            this._sendCommand('STARTTLS');
+            this._responseActions.push(this._actionSTARTTLS);
+            return;
+        }
+
+        // Detect if the server supports SMTPUTF8
+        if (/[ \-]SMTPUTF8\b/mi.test(str)) {
+            this._supportedExtensions.push('SMTPUTF8');
+        }
+
+        // Detect if the server supports DSN
+        if (/[ \-]DSN\b/mi.test(str)) {
+            this._supportedExtensions.push('DSN');
+        }
+
+        // Detect if the server supports 8BITMIME
+        if (/[ \-]8BITMIME\b/mi.test(str)) {
+            this._supportedExtensions.push('8BITMIME');
+        }
+
+        // Detect if the server supports PIPELINING
+        if (/[ \-]PIPELINING\b/mi.test(str)) {
+            this._supportedExtensions.push('PIPELINING');
+        }
+
+        // Detect if the server supports PLAIN auth
+        if (/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)PLAIN/i.test(str)) {
+            this._supportedAuth.push('PLAIN');
+        }
+
+        // Detect if the server supports LOGIN auth
+        if (/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)LOGIN/i.test(str)) {
+            this._supportedAuth.push('LOGIN');
+        }
+
+        // Detect if the server supports CRAM-MD5 auth
+        if (/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)CRAM-MD5/i.test(str)) {
+            this._supportedAuth.push('CRAM-MD5');
+        }
+
+        // Detect if the server supports XOAUTH2 auth
+        if (/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)XOAUTH2/i.test(str)) {
+            this._supportedAuth.push('XOAUTH2');
+        }
+
+        // Detect if the server supports SIZE extensions (and the max allowed size)
+        if ((match = str.match(/[ \-]SIZE(?:[ \t]+(\d+))?/mi))) {
+            this._supportedExtensions.push('SIZE');
+            this._maxAllowedSize = Number(match[1]) || 0;
+        }
+
+        this.emit('connect');
+    }
+
+    /**
+     * Handles server response for HELO command. If it yielded in
+     * error, emit 'error', otherwise move into the authentication phase.
+     *
+     * @param {String} str Message from the server
+     */
+    _actionHELO(str) {
+        if (str.charAt(0) !== '2') {
+            this._onError(new Error('Invalid response for EHLO/HELO:\n' + str), 'EPROTOCOL', str, 'HELO');
+            return;
+        }
+
+        this.emit('connect');
+    }
+
+    /**
+     * Handles server response for STARTTLS command. If there's an error
+     * try HELO instead, otherwise initiate TLS upgrade. If the upgrade
+     * succeedes restart the EHLO
+     *
+     * @param {String} str Message from the server
+     */
+    _actionSTARTTLS(str) {
+        if (str.charAt(0) !== '2') {
+            if (this.options.opportunisticTLS) {
+                this.logger.info({
+                    tnx: 'smtp'
+                }, 'Failed STARTTLS upgrade, continuing unencrypted');
+                return this.emit('connect');
+            }
+            this._onError(new Error('Error upgrading connection with STARTTLS'), 'ETLS', str, 'STARTTLS');
+            return;
+        }
+
+        this._upgradeConnection((err, secured) => {
+            if (err) {
+                this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'STARTTLS');
+                return;
+            }
+
+            this.logger.info({
+                tnx: 'smtp'
+            }, 'Connection upgraded with STARTTLS');
+
+            if (secured) {
+                // restart session
+                if (this.options.lmtp) {
+                    this._responseActions.push(this._actionLHLO);
+                    this._sendCommand('LHLO ' + this.name);
+                } else {
+                    this._responseActions.push(this._actionEHLO);
+                    this._sendCommand('EHLO ' + this.name);
+                }
+            } else {
+                this.emit('connect');
+            }
+        });
+    }
+
+    /**
+     * Handle the response for AUTH LOGIN command. We are expecting
+     * '334 VXNlcm5hbWU6' (base64 for 'Username:'). Data to be sent as
+     * response needs to be base64 encoded username. We do not need
+     * exact match but settle with 334 response in general as some
+     * hosts invalidly use a longer message than VXNlcm5hbWU6
+     *
+     * @param {String} str Message from the server
+     */
+    _actionAUTH_LOGIN_USER(str, callback) {
+        if (!/^334[ \-]/.test(str)) { // expecting '334 VXNlcm5hbWU6'
+            callback(this._formatError('Invalid login sequence while waiting for "334 VXNlcm5hbWU6"', 'EAUTH', str, 'AUTH LOGIN'));
+            return;
+        }
+
+        this._responseActions.push(str => {
+            this._actionAUTH_LOGIN_PASS(str, callback);
+        });
+
+        this._sendCommand(new Buffer(this._auth.credentials.user + '', 'utf-8').toString('base64'));
+    }
+
+    /**
+     * Handle the response for AUTH CRAM-MD5 command. We are expecting
+     * '334 <challenge string>'. Data to be sent as response needs to be
+     * base64 decoded challenge string, MD5 hashed using the password as
+     * a HMAC key, prefixed by the username and a space, and finally all
+     * base64 encoded again.
+     *
+     * @param {String} str Message from the server
+     */
+    _actionAUTH_CRAM_MD5(str, callback) {
+        let challengeMatch = str.match(/^334\s+(.+)$/);
+        let challengeString = '';
+
+        if (!challengeMatch) {
+            return callback(this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH CRAM-MD5'));
+        } else {
+            challengeString = challengeMatch[1];
+        }
+
+        // Decode from base64
+        let base64decoded = new Buffer(challengeString, 'base64').toString('ascii'),
+            hmac_md5 = crypto.createHmac('md5', this._auth.credentials.pass);
+
+        hmac_md5.update(base64decoded);
+
+        let hex_hmac = hmac_md5.digest('hex');
+        let prepended = this._auth.credentials.user + ' ' + hex_hmac;
+
+        this._responseActions.push(str => {
+            this._actionAUTH_CRAM_MD5_PASS(str, callback);
+        });
+
+
+        this._sendCommand(new Buffer(prepended).toString('base64'));
+    }
+
+    /**
+     * Handles the response to CRAM-MD5 authentication, if there's no error,
+     * the user can be considered logged in. Start waiting for a message to send
+     *
+     * @param {String} str Message from the server
+     */
+    _actionAUTH_CRAM_MD5_PASS(str, callback) {
+        if (!str.match(/^235\s+/)) {
+            return callback(this._formatError('Invalid login sequence while waiting for "235"', 'EAUTH', str, 'AUTH CRAM-MD5'));
+        }
+
+        this.logger.info({
+            tnx: 'smtp',
+            username: this._auth.user,
+            action: 'authenticated',
+            method: this._authMethod
+        }, 'User %s authenticated', JSON.stringify(this._auth.user));
+        this.authenticated = true;
+        callback(null, true);
+    }
+
+    /**
+     * Handle the response for AUTH LOGIN command. We are expecting
+     * '334 UGFzc3dvcmQ6' (base64 for 'Password:'). Data to be sent as
+     * response needs to be base64 encoded password.
+     *
+     * @param {String} str Message from the server
+     */
+    _actionAUTH_LOGIN_PASS(str, callback) {
+        if (!/^334[ \-]/.test(str)) { // expecting '334 UGFzc3dvcmQ6'
+            return callback(this._formatError('Invalid login sequence while waiting for "334 UGFzc3dvcmQ6"', 'EAUTH', str, 'AUTH LOGIN'));
+        }
+
+        this._responseActions.push(str => {
+            this._actionAUTHComplete(str, callback);
+        });
+
+        this._sendCommand(new Buffer(this._auth.credentials.pass + '', 'utf-8').toString('base64'));
+    }
+
+    /**
+     * Handles the response for authentication, if there's no error,
+     * the user can be considered logged in. Start waiting for a message to send
+     *
+     * @param {String} str Message from the server
+     */
+    _actionAUTHComplete(str, isRetry, callback) {
+        if (!callback && typeof isRetry === 'function') {
+            callback = isRetry;
+            isRetry = false;
+        }
+
+        if (str.substr(0, 3) === '334') {
+            this._responseActions.push(str => {
+                if (isRetry || this._authMethod !== 'XOAUTH2') {
+                    this._actionAUTHComplete(str, true, callback);
+                } else {
+                    // fetch a new OAuth2 access token
+                    setImmediate(() => this._handleXOauth2Token(true, callback));
+                }
+            });
+            this._sendCommand('');
+            return;
+        }
+
+        if (str.charAt(0) !== '2') {
+            this.logger.info({
+                tnx: 'smtp',
+                username: this._auth.user,
+                action: 'authfail',
+                method: this._authMethod
+            }, 'User %s failed to authenticate', JSON.stringify(this._auth.user));
+            return callback(this._formatError('Invalid login', 'EAUTH', str, 'AUTH ' + this._authMethod));
+        }
+
+        this.logger.info({
+            tnx: 'smtp',
+            username: this._auth.user,
+            action: 'authenticated',
+            method: this._authMethod
+        }, 'User %s authenticated', JSON.stringify(this._auth.user));
+        this.authenticated = true;
+        callback(null, true);
+    }
+
+    /**
+     * Handle response for a MAIL FROM: command
+     *
+     * @param {String} str Message from the server
+     */
+    _actionMAIL(str, callback) {
+        let message, curRecipient;
+        if (Number(str.charAt(0)) !== 2) {
+            if (this._usingSmtpUtf8 && /^550 /.test(str) && /[\x80-\uFFFF]/.test(this._envelope.from)) {
+                message = 'Internationalized mailbox name not allowed';
+            } else {
+                message = 'Mail command failed';
+            }
+            return callback(this._formatError(message, 'EENVELOPE', str, 'MAIL FROM'));
+        }
+
+        if (!this._envelope.rcptQueue.length) {
+            return callback(this._formatError('Can\'t send mail - no recipients defined', 'EENVELOPE', false, 'API'));
+        } else {
+            this._recipientQueue = [];
+
+            if (this._supportedExtensions.includes('PIPELINING')) {
+                while (this._envelope.rcptQueue.length) {
+                    curRecipient = this._envelope.rcptQueue.shift();
+                    this._recipientQueue.push(curRecipient);
+                    this._responseActions.push(str => {
+                        this._actionRCPT(str, callback);
+                    });
+                    this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
+                }
+            } else {
+                curRecipient = this._envelope.rcptQueue.shift();
+                this._recipientQueue.push(curRecipient);
+                this._responseActions.push(str => {
+                    this._actionRCPT(str, callback);
+                });
+                this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
+            }
+        }
+    }
+
+    /**
+     * Handle response for a RCPT TO: command
+     *
+     * @param {String} str Message from the server
+     */
+    _actionRCPT(str, callback) {
+        let message, err, curRecipient = this._recipientQueue.shift();
+        if (Number(str.charAt(0)) !== 2) {
+            // this is a soft error
+            if (this._usingSmtpUtf8 && /^553 /.test(str) && /[\x80-\uFFFF]/.test(curRecipient)) {
+                message = 'Internationalized mailbox name not allowed';
+            } else {
+                message = 'Recipient command failed';
+            }
+            this._envelope.rejected.push(curRecipient);
+            // store error for the failed recipient
+            err = this._formatError(message, 'EENVELOPE', str, 'RCPT TO');
+            err.recipient = curRecipient;
+            this._envelope.rejectedErrors.push(err);
+        } else {
+            this._envelope.accepted.push(curRecipient);
+        }
+
+        if (!this._envelope.rcptQueue.length && !this._recipientQueue.length) {
+            if (this._envelope.rejected.length < this._envelope.to.length) {
+                this._responseActions.push(str => {
+                    this._actionDATA(str, callback);
+                });
+                this._sendCommand('DATA');
+            } else {
+                err = this._formatError('Can\'t send mail - all recipients were rejected', 'EENVELOPE', str, 'RCPT TO');
+                err.rejected = this._envelope.rejected;
+                err.rejectedErrors = this._envelope.rejectedErrors;
+                return callback(err);
+            }
+        } else if (this._envelope.rcptQueue.length) {
+            curRecipient = this._envelope.rcptQueue.shift();
+            this._recipientQueue.push(curRecipient);
+            this._responseActions.push(str => {
+                this._actionRCPT(str, callback);
+            });
+            this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
+        }
+    }
+
+    /**
+     * Handle response for a DATA command
+     *
+     * @param {String} str Message from the server
+     */
+    _actionDATA(str, callback) {
+        // response should be 354 but according to this issue https://github.com/eleith/emailjs/issues/24
+        // some servers might use 250 instead, so lets check for 2 or 3 as the first digit
+        if (!/^[23]/.test(str)) {
+            return callback(this._formatError('Data command failed', 'EENVELOPE', str, 'DATA'));
+        }
+
+        let response = {
+            accepted: this._envelope.accepted,
+            rejected: this._envelope.rejected
+        };
+
+        if (this._envelope.rejectedErrors.length) {
+            response.rejectedErrors = this._envelope.rejectedErrors;
+        }
+
+        callback(null, response);
+    }
+
+    /**
+     * Handle response for a DATA stream when using SMTP
+     * We expect a single response that defines if the sending succeeded or failed
+     *
+     * @param {String} str Message from the server
+     */
+    _actionSMTPStream(str, callback) {
+        if (Number(str.charAt(0)) !== 2) {
+            // Message failed
+            return callback(this._formatError('Message failed', 'EMESSAGE', str, 'DATA'));
+        } else {
+            // Message sent succesfully
+            return callback(null, str);
+        }
+    }
+
+    /**
+     * Handle response for a DATA stream
+     * We expect a separate response for every recipient. All recipients can either
+     * succeed or fail separately
+     *
+     * @param {String} recipient The recipient this response applies to
+     * @param {Boolean} final Is this the final recipient?
+     * @param {String} str Message from the server
+     */
+    _actionLMTPStream(recipient, final, str, callback) {
+        let err;
+        if (Number(str.charAt(0)) !== 2) {
+            // Message failed
+            err = this._formatError('Message failed for recipient ' + recipient, 'EMESSAGE', str, 'DATA');
+            err.recipient = recipient;
+            this._envelope.rejected.push(recipient);
+            this._envelope.rejectedErrors.push(err);
+            for (let i = 0, len = this._envelope.accepted.length; i < len; i++) {
+                if (this._envelope.accepted[i] === recipient) {
+                    this._envelope.accepted.splice(i, 1);
+                }
+            }
+        }
+        if (final) {
+            return callback(null, str);
+        }
+    }
+
+    _handleXOauth2Token(isRetry, callback) {
+        this._auth.oauth2.getToken(isRetry, (err, accessToken) => {
+            if (err) {
+                this.logger.info({
+                    tnx: 'smtp',
+                    username: this._auth.user,
+                    action: 'authfail',
+                    method: this._authMethod
+                }, 'User %s failed to authenticate', JSON.stringify(this._auth.user));
+                return callback(this._formatError(err, 'EAUTH', false, 'AUTH XOAUTH2'));
+            }
+            this._responseActions.push(str => {
+                this._actionAUTHComplete(str, isRetry, callback);
+            });
+            this._sendCommand('AUTH XOAUTH2 ' + this._auth.oauth2.buildXOAuth2Token(accessToken));
+        });
+    }
+
+    _getHostname() {
+        // defaul hostname is machine hostname or [IP]
+        let defaultHostname = os.hostname() || '';
+
+        // ignore if not FQDN
+        if (defaultHostname.indexOf('.') < 0) {
+            defaultHostname = '[127.0.0.1]';
+        }
+
+        // IP should be enclosed in []
+        if (defaultHostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
+            defaultHostname = '[' + defaultHostname + ']';
+        }
+
+        return defaultHostname;
+    }
+}
+
+module.exports = SMTPConnection;
diff --git a/node_modules/nodemailer/lib/smtp-pool/index.js b/node_modules/nodemailer/lib/smtp-pool/index.js
new file mode 100644
index 0000000..8e3155e
--- /dev/null
+++ b/node_modules/nodemailer/lib/smtp-pool/index.js
@@ -0,0 +1,536 @@
+'use strict';
+
+const EventEmitter = require('events');
+const PoolResource = require('./pool-resource');
+const SMTPConnection = require('../smtp-connection');
+const wellKnown = require('../well-known');
+const shared = require('../shared');
+const packageData = require('../../package.json');
+
+/**
+ * Creates a SMTP pool transport object for Nodemailer
+ *
+ * @constructor
+ * @param {Object} options SMTP Connection options
+ */
+class SMTPPool extends EventEmitter {
+    constructor(options) {
+        super();
+
+        options = options || {};
+        if (typeof options === 'string') {
+            options = {
+                url: options
+            };
+        }
+
+        let urlData;
+        let service = options.service;
+
+        if (typeof options.getSocket === 'function') {
+            this.getSocket = options.getSocket;
+        }
+
+        if (options.url) {
+            urlData = shared.parseConnectionUrl(options.url);
+            service = service || urlData.service;
+        }
+
+        this.options = shared.assign(
+            false, // create new object
+            options, // regular options
+            urlData, // url options
+            service && wellKnown(service) // wellknown options
+        );
+
+        this.options.maxConnections = this.options.maxConnections || 5;
+        this.options.maxMessages = this.options.maxMessages || 100;
+
+        this.logger = shared.getLogger(this.options, {
+            component: this.options.component || 'smtp-pool'
+        });
+
+        // temporary object
+        let connection = new SMTPConnection(this.options);
+
+        this.name = 'SMTP (pool)';
+        this.version = packageData.version + '[client:' + connection.version + ']';
+
+        this._rateLimit = {
+            counter: 0,
+            timeout: null,
+            waiting: [],
+            checkpoint: false,
+            delta: Number(this.options.rateDelta) || 1000,
+            limit: Number(this.options.rateLimit) || 0
+        };
+        this._closed = false;
+        this._queue = [];
+        this._connections = [];
+        this._connectionCounter = 0;
+
+        this.idling = true;
+
+        setImmediate(() => {
+            if (this.idling) {
+                this.emit('idle');
+            }
+        });
+    }
+
+    /**
+     * Placeholder function for creating proxy sockets. This method immediatelly returns
+     * without a socket
+     *
+     * @param {Object} options Connection options
+     * @param {Function} callback Callback function to run with the socket keys
+     */
+    getSocket(options, callback) {
+        // return immediatelly
+        return setImmediate(() => callback(null, false));
+    }
+
+    /**
+     * Queues an e-mail to be sent using the selected settings
+     *
+     * @param {Object} mail Mail object
+     * @param {Function} callback Callback function
+     */
+    send(mail, callback) {
+        if (this._closed) {
+            return false;
+        }
+
+        this._queue.push({
+            mail,
+            callback
+        });
+
+        if (this.idling && this._queue.length >= this.options.maxConnections) {
+            this.idling = false;
+        }
+
+        setImmediate(() => this._processMessages());
+
+        return true;
+    }
+
+    /**
+     * Closes all connections in the pool. If there is a message being sent, the connection
+     * is closed later
+     */
+    close() {
+        let connection;
+        let len = this._connections.length;
+        this._closed = true;
+
+        // clear rate limit timer if it exists
+        clearTimeout(this._rateLimit.timeout);
+
+        if (!len && !this._queue.length) {
+            return;
+        }
+
+        // remove all available connections
+        for (let i = len - 1; i >= 0; i--) {
+            if (this._connections[i] && this._connections[i].available) {
+                connection = this._connections[i];
+                connection.close();
+                this.logger.info({
+                    tnx: 'connection',
+                    cid: connection.id,
+                    action: 'removed'
+                }, 'Connection #%s removed', connection.id);
+            }
+        }
+
+        if (len && !this._connections.length) {
+            this.logger.debug({
+                tnx: 'connection'
+            }, 'All connections removed');
+        }
+
+        if (!this._queue.length) {
+            return;
+        }
+
+        // make sure that entire queue would be cleaned
+        let invokeCallbacks = () => {
+            if (!this._queue.length) {
+                this.logger.debug({
+                    tnx: 'connection'
+                }, 'Pending queue entries cleared');
+                return;
+            }
+            let entry = this._queue.shift();
+            if (entry && typeof entry.callback === 'function') {
+                try {
+                    entry.callback(new Error('Connection pool was closed'));
+                } catch (E) {
+                    this.logger.error({
+                        err: E,
+                        tnx: 'callback',
+                        cid: connection.id
+                    }, 'Callback error for #%s: %s', connection.id, E.message);
+                }
+            }
+            setImmediate(invokeCallbacks);
+        };
+        setImmediate(invokeCallbacks);
+    }
+
+    /**
+     * Check the queue and available connections. If there is a message to be sent and there is
+     * an available connection, then use this connection to send the mail
+     */
+    _processMessages() {
+        let connection;
+        let i, len;
+
+        // do nothing if already closed
+        if (this._closed) {
+            return;
+        }
+
+        // do nothing if queue is empty
+        if (!this._queue.length) {
+            if (!this.idling) {
+                // no pending jobs
+                this.idling = true;
+                this.emit('idle');
+            }
+            return;
+        }
+
+        // find first available connection
+        for (i = 0, len = this._connections.length; i < len; i++) {
+            if (this._connections[i].available) {
+                connection = this._connections[i];
+                break;
+            }
+        }
+
+        if (!connection && this._connections.length < this.options.maxConnections) {
+            connection = this._createConnection();
+        }
+
+        if (!connection) {
+            // no more free connection slots available
+            this.idling = false;
+            return;
+        }
+
+        // check if there is free space in the processing queue
+        if (!this.idling && this._queue.length < this.options.maxConnections) {
+            this.idling = true;
+            this.emit('idle');
+        }
+
+        let entry = connection.queueEntry = this._queue.shift();
+        entry.messageId = (connection.queueEntry.mail.message.getHeader('message-id') || '').replace(/[<>\s]/g, '');
+
+        connection.available = false;
+
+        this.logger.debug({
+            tnx: 'pool',
+            cid: connection.id,
+            messageId: entry.messageId,
+            action: 'assign'
+        }, 'Assigned message <%s> to #%s (%s)', entry.messageId, connection.id, connection.messages + 1);
+
+        if (this._rateLimit.limit) {
+            this._rateLimit.counter++;
+            if (!this._rateLimit.checkpoint) {
+                this._rateLimit.checkpoint = Date.now();
+            }
+        }
+
+        connection.send(entry.mail, (err, info) => {
+            // only process callback if current handler is not changed
+            if (entry === connection.queueEntry) {
+                try {
+                    entry.callback(err, info);
+                } catch (E) {
+                    this.logger.error({
+                        err: E,
+                        tnx: 'callback',
+                        cid: connection.id
+                    }, 'Callback error for #%s: %s', connection.id, E.message);
+                }
+                connection.queueEntry = false;
+            }
+        });
+    }
+
+    /**
+     * Creates a new pool resource
+     */
+    _createConnection() {
+        let connection = new PoolResource(this);
+
+        connection.id = ++this._connectionCounter;
+
+        this.logger.info({
+            tnx: 'pool',
+            cid: connection.id,
+            action: 'conection'
+        }, 'Created new pool resource #%s', connection.id);
+
+        // resource comes available
+        connection.on('available', () => {
+            this.logger.debug({
+                tnx: 'connection',
+                cid: connection.id,
+                action: 'available'
+            }, 'Connection #%s became available', connection.id);
+
+            if (this._closed) {
+                // if already closed run close() that will remove this connections from connections list
+                this.close();
+            } else {
+                // check if there's anything else to send
+                this._processMessages();
+            }
+        });
+
+        // resource is terminated with an error
+        connection.once('error', err => {
+            if (err.code !== 'EMAXLIMIT') {
+                this.logger.error({
+                    err,
+                    tnx: 'pool',
+                    cid: connection.id
+                }, 'Pool Error for #%s: %s', connection.id, err.message);
+            } else {
+                this.logger.debug({
+                    tnx: 'pool',
+                    cid: connection.id,
+                    action: 'maxlimit'
+                }, 'Max messages limit exchausted for #%s', connection.id);
+            }
+
+            if (connection.queueEntry) {
+                try {
+                    connection.queueEntry.callback(err);
+                } catch (E) {
+                    this.logger.error({
+                        err: E,
+                        tnx: 'callback',
+                        cid: connection.id
+                    }, 'Callback error for #%s: %s', connection.id, E.message);
+                }
+                connection.queueEntry = false;
+            }
+
+            // remove the erroneus connection from connections list
+            this._removeConnection(connection);
+
+            this._continueProcessing();
+        });
+
+        connection.once('close', () => {
+            this.logger.info({
+                tnx: 'connection',
+                cid: connection.id,
+                action: 'closed'
+            }, 'Connection #%s was closed', connection.id);
+
+            this._removeConnection(connection);
+
+            if (connection.queueEntry) {
+                // If the connection closed when sending, add the message to the queue again
+                // Note that we must wait a bit.. because the callback of the 'error' handler might be called
+                // in the next event loop
+                setTimeout(() => {
+                    if (connection.queueEntry) {
+                        this.logger.debug({
+                            tnx: 'pool',
+                            cid: connection.id,
+                            messageId: connection.queueEntry.messageId,
+                            action: 'requeue'
+                        }, 'Re-queued message <%s> for #%s', connection.queueEntry.messageId, connection.id);
+                        this._queue.unshift(connection.queueEntry);
+                        connection.queueEntry = false;
+                    }
+                    this._continueProcessing();
+                }, 50);
+            } else {
+                this._continueProcessing();
+            }
+        });
+
+        this._connections.push(connection);
+
+        return connection;
+    }
+
+    /**
+     * Continue to process message if the pool hasn't closed
+     */
+    _continueProcessing() {
+        if (this._closed) {
+            this.close();
+        } else {
+            setTimeout(() => this._processMessages(), 100);
+        }
+    }
+
+    /**
+     * Remove resource from pool
+     *
+     * @param {Object} connection The PoolResource to remove
+     */
+    _removeConnection(connection) {
+        let index = this._connections.indexOf(connection);
+
+        if (index !== -1) {
+            this._connections.splice(index, 1);
+        }
+    }
+
+    /**
+     * Checks if connections have hit current rate limit and if so, queues the availability callback
+     *
+     * @param {Function} callback Callback function to run once rate limiter has been cleared
+     */
+    _checkRateLimit(callback) {
+        if (!this._rateLimit.limit) {
+            return callback();
+        }
+
+        let now = Date.now();
+
+        if (this._rateLimit.counter < this._rateLimit.limit) {
+            return callback();
+        }
+
+        this._rateLimit.waiting.push(callback);
+
+        if (this._rateLimit.checkpoint <= now - this._rateLimit.delta) {
+            return this._clearRateLimit();
+        } else if (!this._rateLimit.timeout) {
+            this._rateLimit.timeout = setTimeout(() => this._clearRateLimit(), this._rateLimit.delta - (now - this._rateLimit.checkpoint));
+            this._rateLimit.checkpoint = now;
+        }
+    }
+
+    /**
+     * Clears current rate limit limitation and runs paused callback
+     */
+    _clearRateLimit() {
+        clearTimeout(this._rateLimit.timeout);
+        this._rateLimit.timeout = null;
+        this._rateLimit.counter = 0;
+        this._rateLimit.checkpoint = false;
+
+        // resume all paused connections
+        while (this._rateLimit.waiting.length) {
+            let cb = this._rateLimit.waiting.shift();
+            setImmediate(cb);
+        }
+    }
+
+    /**
+     * Returns true if there are free slots in the queue
+     */
+    isIdle() {
+        return this.idling;
+    }
+
+    /**
+     * Verifies SMTP configuration
+     *
+     * @param {Function} callback Callback function
+     */
+    verify(callback) {
+        let promise;
+
+        if (!callback && typeof Promise === 'function') {
+            promise = new Promise((resolve, reject) => {
+                callback = shared.callbackPromise(resolve, reject);
+            });
+        }
+
+        let auth = new PoolResource(this).auth;
+
+        this.getSocket(this.options, (err, socketOptions) => {
+            if (err) {
+                return callback(err);
+            }
+
+            let options = this.options;
+            if (socketOptions && socketOptions.connection) {
+                this.logger.info({
+                    tnx: 'proxy',
+                    remoteAddress: socketOptions.connection.remoteAddress,
+                    remotePort: socketOptions.connection.remotePort,
+                    destHost: options.host || '',
+                    destPort: options.port || '',
+                    action: 'connected'
+                }, 'Using proxied socket from %s:%s to %s:%s', socketOptions.connection.remoteAddress, socketOptions.connection.remotePort, options.host || '', options.port || '');
+                options = shared.assign(false, options);
+                Object.keys(socketOptions).forEach(key => {
+                    options[key] = socketOptions[key];
+                });
+            }
+
+            let connection = new SMTPConnection(options);
+            let returned = false;
+
+            connection.once('error', err => {
+                if (returned) {
+                    return;
+                }
+                returned = true;
+                connection.close();
+                return callback(err);
+            });
+
+            connection.once('end', () => {
+                if (returned) {
+                    return;
+                }
+                returned = true;
+                return callback(new Error('Connection closed'));
+            });
+
+            let finalize = () => {
+                if (returned) {
+                    return;
+                }
+                returned = true;
+                connection.quit();
+                return callback(null, true);
+            };
+
+            connection.connect(() => {
+                if (returned) {
+                    return;
+                }
+
+                if (auth) {
+                    connection.login(auth, err => {
+                        if (returned) {
+                            return;
+                        }
+
+                        if (err) {
+                            returned = true;
+                            connection.close();
+                            return callback(err);
+                        }
+
+                        finalize();
+                    });
+                } else {
+                    finalize();
+                }
+            });
+        });
+
+        return promise;
+    }
+}
+
+// expose to the world
+module.exports = SMTPPool;
diff --git a/node_modules/nodemailer/lib/smtp-pool/pool-resource.js b/node_modules/nodemailer/lib/smtp-pool/pool-resource.js
new file mode 100644
index 0000000..98a3a21
--- /dev/null
+++ b/node_modules/nodemailer/lib/smtp-pool/pool-resource.js
@@ -0,0 +1,230 @@
+'use strict';
+
+const SMTPConnection = require('../smtp-connection');
+const assign = require('../shared').assign;
+const XOAuth2 = require('../xoauth2');
+const EventEmitter = require('events');
+
+/**
+ * Creates an element for the pool
+ *
+ * @constructor
+ * @param {Object} options SMTPPool instance
+ */
+class PoolResource extends EventEmitter {
+    constructor(pool) {
+        super();
+
+        this.pool = pool;
+        this.options = pool.options;
+        this.logger = this.pool.logger;
+
+        if (this.options.auth) {
+            switch ((this.options.auth.type || '').toString().toUpperCase()) {
+                case 'OAUTH2':
+                    {
+                        let oauth2 = new XOAuth2(this.options.auth, this.logger);
+                        oauth2.provisionCallback = this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb') || oauth2.provisionCallback;
+                        this.auth = {
+                            type: 'OAUTH2',
+                            user: this.options.auth.user,
+                            oauth2,
+                            method: 'XOAUTH2'
+                        };
+                        oauth2.on('token', token => this.pool.mailer.emit('token', token));
+                        oauth2.on('error', err => this.emit('error', err));
+                        break;
+                    }
+                default:
+                    this.auth = {
+                        type: 'LOGIN',
+                        user: this.options.auth.user,
+                        credentials: {
+                            user: this.options.auth.user || '',
+                            pass: this.options.auth.pass
+                        },
+                        method: (this.options.auth.method || '').trim().toUpperCase() || false
+                    };
+            }
+        }
+
+        this._connection = false;
+        this._connected = false;
+
+        this.messages = 0;
+        this.available = true;
+    }
+
+    /**
+     * Initiates a connection to the SMTP server
+     *
+     * @param {Function} callback Callback function to run once the connection is established or failed
+     */
+    connect(callback) {
+        this.pool.getSocket(this.options, (err, socketOptions) => {
+            if (err) {
+                return callback(err);
+            }
+
+            let returned = false;
+            let options = this.options;
+            if (socketOptions && socketOptions.connection) {
+                this.logger.info({
+                    tnx: 'proxy',
+                    remoteAddress: socketOptions.connection.remoteAddress,
+                    remotePort: socketOptions.connection.remotePort,
+                    destHost: options.host || '',
+                    destPort: options.port || '',
+                    action: 'connected'
+                }, 'Using proxied socket from %s:%s to %s:%s', socketOptions.connection.remoteAddress, socketOptions.connection.remotePort, options.host || '', options.port || '');
+
+                options = assign(false, options);
+                Object.keys(socketOptions).forEach(key => {
+                    options[key] = socketOptions[key];
+                });
+            }
+
+            this.connection = new SMTPConnection(options);
+
+            this.connection.once('error', err => {
+                this.emit('error', err);
+                if (returned) {
+                    return;
+                }
+                returned = true;
+                return callback(err);
+            });
+
+            this.connection.once('end', () => {
+                this.close();
+                if (returned) {
+                    return;
+                }
+                returned = true;
+                setTimeout(() => {
+                    if (returned) {
+                        return;
+                    }
+                    // still have not returned, this means we have an unexpected connection close
+                    let err = new Error('Unexpected socket close');
+                    if (this.connection && this.connection._socket && this.connection._socket.upgrading) {
+                        // starttls connection errors
+                        err.code = 'ETLS';
+                    }
+                    callback(err);
+                }, 1000).unref();
+            });
+
+            this.connection.connect(() => {
+                if (returned) {
+                    return;
+                }
+
+                if (this.auth) {
+                    this.connection.login(this.auth, err => {
+                        if (returned) {
+                            return;
+                        }
+                        returned = true;
+
+                        if (err) {
+                            this.connection.close();
+                            this.emit('error', err);
+                            return callback(err);
+                        }
+
+                        this._connected = true;
+                        callback(null, true);
+                    });
+                } else {
+                    returned = true;
+                    this._connected = true;
+                    return callback(null, true);
+                }
+            });
+        });
+    }
+
+    /**
+     * Sends an e-mail to be sent using the selected settings
+     *
+     * @param {Object} mail Mail object
+     * @param {Function} callback Callback function
+     */
+    send(mail, callback) {
+        if (!this._connected) {
+            return this.connect(err => {
+                if (err) {
+                    return callback(err);
+                }
+                return this.send(mail, callback);
+            });
+        }
+
+        let envelope = mail.message.getEnvelope();
+        let messageId = mail.message.messageId();
+
+        let recipients = [].concat(envelope.to || []);
+        if (recipients.length > 3) {
+            recipients.push('...and ' + recipients.splice(2).length + ' more');
+        }
+        this.logger.info({
+            tnx: 'send',
+            messageId,
+            cid: this.id
+        }, 'Sending message %s using #%s to <%s>', messageId, this.id, recipients.join(', '));
+
+        if (mail.data.dsn) {
+            envelope.dsn = mail.data.dsn;
+        }
+
+        this.connection.send(envelope, mail.message.createReadStream(), (err, info) => {
+            this.messages++;
+
+            if (err) {
+                this.connection.close();
+                this.emit('error', err);
+                return callback(err);
+            }
+
+            info.envelope = {
+                from: envelope.from,
+                to: envelope.to
+            };
+            info.messageId = messageId;
+
+            setImmediate(() => {
+                let err;
+                if (this.messages >= this.options.maxMessages) {
+                    err = new Error('Resource exhausted');
+                    err.code = 'EMAXLIMIT';
+                    this.connection.close();
+                    this.emit('error', err);
+                } else {
+                    this.pool._checkRateLimit(() => {
+                        this.available = true;
+                        this.emit('available');
+                    });
+                }
+            });
+
+            callback(null, info);
+        });
+    }
+
+    /**
+     * Closes the connection
+     */
+    close() {
+        this._connected = false;
+        if (this.auth && this.auth.oauth2) {
+            this.auth.oauth2.removeAllListeners();
+        }
+        if (this.connection) {
+            this.connection.close();
+        }
+        this.emit('close');
+    }
+}
+
+module.exports = PoolResource;
diff --git a/node_modules/nodemailer/lib/smtp-transport/index.js b/node_modules/nodemailer/lib/smtp-transport/index.js
new file mode 100644
index 0000000..5376e5a
--- /dev/null
+++ b/node_modules/nodemailer/lib/smtp-transport/index.js
@@ -0,0 +1,372 @@
+'use strict';
+
+const EventEmitter = require('events');
+const SMTPConnection = require('../smtp-connection');
+const wellKnown = require('../well-known');
+const shared = require('../shared');
+const XOAuth2 = require('../xoauth2');
+const packageData = require('../../package.json');
+
+/**
+ * Creates a SMTP transport object for Nodemailer
+ *
+ * @constructor
+ * @param {Object} options Connection options
+ */
+class SMTPTransport extends EventEmitter {
+    constructor(options) {
+        super();
+
+        options = options || {};
+        if (typeof options === 'string') {
+            options = {
+                url: options
+            };
+        }
+
+        let urlData;
+        let service = options.service;
+
+        if (typeof options.getSocket === 'function') {
+            this.getSocket = options.getSocket;
+        }
+
+        if (options.url) {
+            urlData = shared.parseConnectionUrl(options.url);
+            service = service || urlData.service;
+        }
+
+        this.options = shared.assign(
+            false, // create new object
+            options, // regular options
+            urlData, // url options
+            service && wellKnown(service) // wellknown options
+        );
+
+        this.logger = shared.getLogger(this.options, {
+            component: this.options.component || 'smtp-transport'
+        });
+
+        // temporary object
+        let connection = new SMTPConnection(this.options);
+
+        this.name = 'SMTP';
+        this.version = packageData.version + '[client:' + connection.version + ']';
+
+        if (this.options.auth) {
+            this.auth = this.getAuth({});
+        }
+    }
+
+    /**
+     * Placeholder function for creating proxy sockets. This method immediatelly returns
+     * without a socket
+     *
+     * @param {Object} options Connection options
+     * @param {Function} callback Callback function to run with the socket keys
+     */
+    getSocket(options, callback) {
+        // return immediatelly
+        return setImmediate(() => callback(null, false));
+    }
+
+    getAuth(authOpts) {
+        if (!authOpts) {
+            return this.auth;
+        }
+
+        let hasAuth = false;
+        let authData = {};
+
+        if (this.options.auth && typeof this.options.auth === 'object') {
+            Object.keys(this.options.auth).forEach(key => {
+                hasAuth = true;
+                authData[key] = this.options.auth[key];
+            });
+        }
+
+        if (authOpts && typeof authOpts === 'object') {
+            Object.keys(authOpts).forEach(key => {
+                hasAuth = true;
+                authData[key] = authOpts[key];
+            });
+        }
+
+        if (!hasAuth) {
+            return false;
+        }
+
+        switch ((authData.type || '').toString().toUpperCase()) {
+            case 'OAUTH2':
+                {
+                    if (!authData.service && !authData.user) {
+                        return false;
+                    }
+                    let oauth2 = new XOAuth2(authData, this.logger);
+                    oauth2.provisionCallback = this.mailer && this.mailer.get('oauth2_provision_cb') || oauth2.provisionCallback;
+                    oauth2.on('token', token => this.mailer.emit('token', token));
+                    oauth2.on('error', err => this.emit('error', err));
+                    return {
+                        type: 'OAUTH2',
+                        user: authData.user,
+                        oauth2,
+                        method: 'XOAUTH2'
+                    };
+                }
+            default:
+                return {
+                    type: 'LOGIN',
+                    user: authData.user,
+                    credentials: {
+                        user: authData.user || '',
+                        pass: authData.pass
+                    },
+                    method: (authData.method || '').trim().toUpperCase() || false
+                };
+        }
+    }
+
+    /**
+     * Sends an e-mail using the selected settings
+     *
+     * @param {Object} mail Mail object
+     * @param {Function} callback Callback function
+     */
+    send(mail, callback) {
+        this.getSocket(this.options, (err, socketOptions) => {
+            if (err) {
+                return callback(err);
+            }
+
+            let returned = false;
+            let options = this.options;
+            if (socketOptions && socketOptions.connection) {
+
+                this.logger.info({
+                    tnx: 'proxy',
+                    remoteAddress: socketOptions.connection.remoteAddress,
+                    remotePort: socketOptions.connection.remotePort,
+                    destHost: options.host || '',
+                    destPort: options.port || '',
+                    action: 'connected'
+                }, 'Using proxied socket from %s:%s to %s:%s', socketOptions.connection.remoteAddress, socketOptions.connection.remotePort, options.host || '', options.port || '');
+
+                // only copy options if we need to modify it
+                options = shared.assign(false, options);
+                Object.keys(socketOptions).forEach(key => {
+                    options[key] = socketOptions[key];
+                });
+            }
+
+            let connection = new SMTPConnection(options);
+
+            connection.once('error', err => {
+                if (returned) {
+                    return;
+                }
+                returned = true;
+                connection.close();
+                return callback(err);
+            });
+
+            connection.once('end', () => {
+                if (returned) {
+                    return;
+                }
+                returned = true;
+                setTimeout(() => {
+                    if (returned) {
+                        return;
+                    }
+                    // still have not returned, this means we have an unexpected connection close
+                    let err = new Error('Unexpected socket close');
+                    if (connection && connection._socket && connection._socket.upgrading) {
+                        // starttls connection errors
+                        err.code = 'ETLS';
+                    }
+                    callback(err);
+                }, 1000).unref();
+            });
+
+            let sendMessage = () => {
+                let envelope = mail.message.getEnvelope();
+                let messageId = mail.message.messageId();
+
+                let recipients = [].concat(envelope.to || []);
+                if (recipients.length > 3) {
+                    recipients.push('...and ' + recipients.splice(2).length + ' more');
+                }
+
+                if (mail.data.dsn) {
+                    envelope.dsn = mail.data.dsn;
+                }
+
+                this.logger.info({
+                    tnx: 'send',
+                    messageId
+                }, 'Sending message %s to <%s>', messageId, recipients.join(', '));
+
+                connection.send(envelope, mail.message.createReadStream(), (err, info) => {
+                    connection.close();
+                    if (err) {
+                        this.logger.error({
+                            err,
+                            tnx: 'send'
+                        }, 'Send error for %s: %s', messageId, err.message);
+                        return callback(err);
+                    }
+                    info.envelope = {
+                        from: envelope.from,
+                        to: envelope.to
+                    };
+                    info.messageId = messageId;
+                    try {
+                        return callback(null, info);
+                    } catch (E) {
+                        this.logger.error({
+                            err: E,
+                            tnx: 'callback'
+                        }, 'Callback error for %s: %s', messageId, E.message);
+                    }
+                });
+            };
+
+            connection.connect(() => {
+                if (returned) {
+                    return;
+                }
+
+                let auth = this.getAuth(mail.data.auth);
+
+                if (auth) {
+                    connection.login(auth, err => {
+                        if (auth && auth !== this.auth && auth.oauth2) {
+                            auth.oauth2.removeAllListeners();
+                        }
+                        if (returned) {
+                            return;
+                        }
+                        returned = true;
+
+                        if (err) {
+                            connection.close();
+                            return callback(err);
+                        }
+
+                        sendMessage();
+                    });
+                } else {
+                    sendMessage();
+                }
+            });
+        });
+    }
+
+    /**
+     * Verifies SMTP configuration
+     *
+     * @param {Function} callback Callback function
+     */
+    verify(callback) {
+        let promise;
+
+        if (!callback && typeof Promise === 'function') {
+            promise = new Promise((resolve, reject) => {
+                callback = shared.callbackPromise(resolve, reject);
+            });
+        }
+
+        this.getSocket(this.options, (err, socketOptions) => {
+            if (err) {
+                return callback(err);
+            }
+
+            let options = this.options;
+            if (socketOptions && socketOptions.connection) {
+                this.logger.info({
+                    tnx: 'proxy',
+                    remoteAddress: socketOptions.connection.remoteAddress,
+                    remotePort: socketOptions.connection.remotePort,
+                    destHost: options.host || '',
+                    destPort: options.port || '',
+                    action: 'connected'
+                }, 'Using proxied socket from %s:%s to %s:%s', socketOptions.connection.remoteAddress, socketOptions.connection.remotePort, options.host || '', options.port || '');
+
+                options = shared.assign(false, options);
+                Object.keys(socketOptions).forEach(key => {
+                    options[key] = socketOptions[key];
+                });
+            }
+
+            let connection = new SMTPConnection(options);
+            let returned = false;
+
+            connection.once('error', err => {
+                if (returned) {
+                    return;
+                }
+                returned = true;
+                connection.close();
+                return callback(err);
+            });
+
+            connection.once('end', () => {
+                if (returned) {
+                    return;
+                }
+                returned = true;
+                return callback(new Error('Connection closed'));
+            });
+
+            let finalize = () => {
+                if (returned) {
+                    return;
+                }
+                returned = true;
+                connection.quit();
+                return callback(null, true);
+            };
+
+            connection.connect(() => {
+                if (returned) {
+                    return;
+                }
+
+                let authData = this.getAuth({});
+
+                if (authData) {
+                    connection.login(authData, err => {
+                        if (returned) {
+                            return;
+                        }
+
+                        if (err) {
+                            returned = true;
+                            connection.close();
+                            return callback(err);
+                        }
+
+                        finalize();
+                    });
+                } else {
+                    finalize();
+                }
+            });
+        });
+
+        return promise;
+    }
+
+    /**
+     * Releases resources
+     */
+    close() {
+        if (this.auth && this.auth.oauth2) {
+            this.auth.oauth2.removeAllListeners();
+        }
+        this.emit('close');
+    }
+}
+
+// expose to the world
+module.exports = SMTPTransport;
diff --git a/node_modules/nodemailer/lib/stream-transport/index.js b/node_modules/nodemailer/lib/stream-transport/index.js
new file mode 100644
index 0000000..3666698
--- /dev/null
+++ b/node_modules/nodemailer/lib/stream-transport/index.js
@@ -0,0 +1,120 @@
+'use strict';
+
+const packageData = require('../../package.json');
+const shared = require('../shared');
+const LeWindows = require('../sendmail-transport/le-windows');
+const LeUnix = require('../sendmail-transport/le-unix');
+
+/**
+ * Generates a Transport object for streaming
+ *
+ * Possible options can be the following:
+ *
+ *  * **buffer** if true, then returns the message as a Buffer object instead of a stream
+ *  * **newline** either 'windows' or 'unix'
+ *
+ * @constructor
+ * @param {Object} optional config parameter for the AWS Sendmail service
+ */
+class SendmailTransport {
+    constructor(options) {
+        options = options || {};
+
+        this.options = options || {};
+
+        this.name = 'StreamTransport';
+        this.version = packageData.version;
+
+        this.logger = shared.getLogger(this.options, {
+            component: this.options.component || 'stream-transport'
+        });
+
+        this.winbreak = ['win', 'windows', 'dos', '\r\n'].includes((options.newline || '').toString().toLowerCase());
+    }
+
+    /**
+     * Compiles a mailcomposer message and forwards it to handler that sends it
+     *
+     * @param {Object} emailMessage MailComposer object
+     * @param {Function} callback Callback function to run when the sending is completed
+     */
+    send(mail, done) {
+        // We probably need this in the output
+        mail.message.keepBcc = true;
+
+        let envelope = mail.data.envelope || mail.message.getEnvelope();
+        let messageId = mail.message.messageId();
+
+        let recipients = [].concat(envelope.to || []);
+        if (recipients.length > 3) {
+            recipients.push('...and ' + recipients.splice(2).length + ' more');
+        }
+        this.logger.info({
+            tnx: 'send',
+            messageId
+        }, 'Sending message %s to <%s> using %s line breaks', messageId, recipients.join(', '), this.winbreak ? '<CR><LF>' : '<LF>');
+
+        setImmediate(() => {
+
+            let sourceStream;
+            let stream;
+            let transform;
+
+            try {
+                transform = this.winbreak ? new LeWindows() : new LeUnix();
+                sourceStream = mail.message.createReadStream();
+                stream = sourceStream.pipe(transform);
+                sourceStream.on('error', err => stream.emit('error', err));
+            } catch (E) {
+                this.logger.error({
+                    err: E,
+                    tnx: 'send',
+                    messageId
+                }, 'Creating send stream failed for %s. %s', messageId, E.message);
+                return done(E);
+            }
+
+            if (!this.options.buffer) {
+                stream.once('error', err => {
+                    this.logger.error({
+                        err,
+                        tnx: 'send',
+                        messageId
+                    }, 'Failed creating message for %s. %s', messageId, err.message);
+                });
+                return done(null, {
+                    envelope: mail.data.envelope || mail.message.getEnvelope(),
+                    messageId,
+                    message: stream
+                });
+            }
+
+            let chunks = [];
+            let chunklen = 0;
+            stream.on('readable', () => {
+                let chunk;
+                while ((chunk = stream.read()) !== null) {
+                    chunks.push(chunk);
+                    chunklen += chunk.length;
+                }
+            });
+
+            stream.once('error', err => {
+                this.logger.error({
+                    err,
+                    tnx: 'send',
+                    messageId
+                }, 'Failed creating message for %s. %s', messageId, err.message);
+                return done(err);
+            });
+
+            stream.on('end', () => done(null, {
+                envelope: mail.data.envelope || mail.message.getEnvelope(),
+                messageId,
+                message: Buffer.concat(chunks, chunklen)
+            }));
+        });
+    }
+}
+
+module.exports = SendmailTransport;
diff --git a/node_modules/nodemailer/lib/well-known/index.js b/node_modules/nodemailer/lib/well-known/index.js
new file mode 100644
index 0000000..0bf91c7
--- /dev/null
+++ b/node_modules/nodemailer/lib/well-known/index.js
@@ -0,0 +1,47 @@
+'use strict';
+
+const services = require('./services.json');
+const normalized = {};
+
+Object.keys(services).forEach(key => {
+    let service = services[key];
+
+    normalized[normalizeKey(key)] = normalizeService(service);
+
+    [].concat(service.aliases || []).forEach(alias => {
+        normalized[normalizeKey(alias)] = normalizeService(service);
+    });
+
+    [].concat(service.domains || []).forEach(domain => {
+        normalized[normalizeKey(domain)] = normalizeService(service);
+    });
+});
+
+function normalizeKey(key) {
+    return key.replace(/[^a-zA-Z0-9.\-]/g, '').toLowerCase();
+}
+
+function normalizeService(service) {
+    let filter = ['domains', 'aliases'];
+    let response = {};
+
+    Object.keys(service).forEach(key => {
+        if (filter.indexOf(key) < 0) {
+            response[key] = service[key];
+        }
+    });
+
+    return response;
+}
+
+/**
+ * Resolves SMTP config for given key. Key can be a name (like 'Gmail'), alias (like 'Google Mail') or
+ * an email address (like 'test@googlemail.com').
+ *
+ * @param {String} key [description]
+ * @returns {Object} SMTP config or false if not found
+ */
+module.exports = function (key) {
+    key = normalizeKey(key.split('@').pop());
+    return normalized[key] || false;
+};
diff --git a/node_modules/nodemailer/lib/well-known/services.json b/node_modules/nodemailer/lib/well-known/services.json
new file mode 100644
index 0000000..f3209ea
--- /dev/null
+++ b/node_modules/nodemailer/lib/well-known/services.json
@@ -0,0 +1,280 @@
+{
+    "1und1": {
+        "host": "smtp.1und1.de",
+        "port": 465,
+        "secure": true,
+        "authMethod": "LOGIN"
+    },
+
+    "AOL": {
+        "domains": [
+            "aol.com"
+        ],
+        "host": "smtp.aol.com",
+        "port": 587
+    },
+
+    "DebugMail": {
+        "host": "debugmail.io",
+        "port": 25
+    },
+
+    "DynectEmail": {
+        "aliases": ["Dynect"],
+        "host": "smtp.dynect.net",
+        "port": 25
+    },
+
+    "FastMail": {
+        "domains": [
+            "fastmail.fm"
+        ],
+        "host": "mail.messagingengine.com",
+        "port": 465,
+        "secure": true
+    },
+
+    "GandiMail": {
+        "aliases": [
+            "Gandi",
+            "Gandi Mail"
+        ],
+        "host": "mail.gandi.net",
+        "port": 587
+    },
+
+    "Gmail": {
+        "aliases": [
+            "Google Mail"
+        ],
+        "domains": [
+            "gmail.com",
+            "googlemail.com"
+        ],
+        "host": "smtp.gmail.com",
+        "port": 465,
+        "secure": true
+    },
+
+    "Godaddy": {
+        "host": "smtpout.secureserver.net",
+        "port": 25
+    },
+
+    "GodaddyAsia": {
+        "host": "smtp.asia.secureserver.net",
+        "port": 25
+    },
+
+    "GodaddyEurope": {
+        "host": "smtp.europe.secureserver.net",
+        "port": 25
+    },
+
+    "hot.ee": {
+        "host": "mail.hot.ee"
+    },
+
+    "Hotmail": {
+        "aliases": [
+            "Outlook",
+            "Outlook.com",
+            "Hotmail.com"
+        ],
+        "domains": [
+            "hotmail.com",
+            "outlook.com"
+        ],
+        "host": "smtp.live.com",
+        "port": 587,
+        "tls": {
+            "ciphers": "SSLv3"
+        }
+    },
+
+    "iCloud": {
+        "aliases": ["Me", "Mac"],
+        "domains": [
+            "me.com",
+            "mac.com"
+        ],
+        "host": "smtp.mail.me.com",
+        "port": 587
+    },
+
+    "mail.ee": {
+        "host": "smtp.mail.ee"
+    },
+
+    "Mail.ru": {
+        "host": "smtp.mail.ru",
+        "port": 465,
+        "secure": true
+    },
+
+    "Maildev": {
+        "port": 1025,
+        "ignoreTLS": true
+    },
+
+    "Mailgun": {
+        "host": "smtp.mailgun.org",
+        "port": 587
+    },
+
+    "Mailjet": {
+        "host": "in.mailjet.com",
+        "port": 587
+    },
+
+    "Mailosaur": {
+        "host": "mailosaur.io",
+        "port": 25
+    },
+
+    "Mandrill": {
+        "host": "smtp.mandrillapp.com",
+        "port": 587
+    },
+
+    "Naver": {
+        "host": "smtp.naver.com",
+        "port": 587
+    },
+
+    "OpenMailBox": {
+        "aliases": [
+            "OMB",
+            "openmailbox.org"
+        ],
+        "host": "smtp.openmailbox.org",
+        "port": 465,
+        "secure": true
+    },
+
+    "Outlook365": {
+        "host": "smtp.office365.com",
+        "port": 587,
+        "secure": false
+    },
+
+    "Postmark": {
+        "aliases": ["PostmarkApp"],
+        "host": "smtp.postmarkapp.com",
+        "port": 2525
+    },
+
+    "QQ": {
+        "domains": [
+            "qq.com"
+        ],
+        "host": "smtp.qq.com",
+        "port": 465,
+        "secure": true
+    },
+
+    "QQex": {
+        "aliases": ["QQ Enterprise"],
+        "domains": [
+            "exmail.qq.com"
+        ],
+        "host": "smtp.exmail.qq.com",
+        "port": 465,
+        "secure": true
+    },
+
+    "SendCloud": {
+        "host": "smtpcloud.sohu.com",
+        "port": 25
+    },
+
+    "SendGrid": {
+        "host": "smtp.sendgrid.net",
+        "port": 587
+    },
+
+    "SendinBlue": {
+        "host": "smtp-relay.sendinblue.com",
+        "port": 587
+    },
+
+    "SendPulse": {
+        "host": "smtp-pulse.com",
+        "port": 465,
+        "secure": true
+    },
+
+    "SES": {
+        "host": "email-smtp.us-east-1.amazonaws.com",
+        "port": 465,
+        "secure": true
+    },
+    "SES-US-EAST-1": {
+        "host": "email-smtp.us-east-1.amazonaws.com",
+        "port": 465,
+        "secure": true
+    },
+    "SES-US-WEST-2": {
+        "host": "email-smtp.us-west-2.amazonaws.com",
+        "port": 465,
+        "secure": true
+    },
+    "SES-EU-WEST-1": {
+        "host": "email-smtp.eu-west-1.amazonaws.com",
+        "port": 465,
+        "secure": true
+    },
+
+    "Sparkpost": {
+        "aliases": [
+            "SparkPost",
+            "SparkPost Mail"
+        ],
+        "domains": [
+            "sparkpost.com"
+        ],
+        "host": "smtp.sparkpostmail.com",
+        "port": 587,
+        "secure": false
+    },
+
+    "Yahoo": {
+        "domains": [
+            "yahoo.com"
+        ],
+        "host": "smtp.mail.yahoo.com",
+        "port": 465,
+        "secure": true
+    },
+
+    "Yandex": {
+        "domains": [
+            "yandex.ru"
+        ],
+        "host": "smtp.yandex.ru",
+        "port": 465,
+        "secure": true
+    },
+
+    "Zoho": {
+        "host": "smtp.zoho.com",
+        "port": 465,
+        "secure": true,
+        "authMethod": "LOGIN"
+    },
+    "126": {
+        "host": "smtp.126.com",
+        "port": 465,
+        "secure": true
+    },
+    "163": {
+        "host": "smtp.163.com",
+        "port": 465,
+        "secure": true
+    },
+    "qiye.aliyun": {
+        "host": "smtp.mxhichina.com",
+        "port": "465",
+        "secure": true
+    }
+}
diff --git a/node_modules/nodemailer/lib/xoauth2/index.js b/node_modules/nodemailer/lib/xoauth2/index.js
new file mode 100644
index 0000000..7078317
--- /dev/null
+++ b/node_modules/nodemailer/lib/xoauth2/index.js
@@ -0,0 +1,306 @@
+'use strict';
+
+const Stream = require('stream').Stream;
+const fetch = require('../fetch');
+const crypto = require('crypto');
+const shared = require('../shared');
+
+/**
+ * XOAUTH2 access_token generator for Gmail.
+ * Create client ID for web applications in Google API console to use it.
+ * See Offline Access for receiving the needed refreshToken for an user
+ * https://developers.google.com/accounts/docs/OAuth2WebServer#offline
+ *
+ * Usage for generating access tokens with a custom method using provisionCallback:
+ * provisionCallback(user, renew, callback)
+ *   * user is the username to get the token for
+ *   * renew is a boolean that if true indicates that existing token failed and needs to be renewed
+ *   * callback is the callback to run with (error, accessToken [, expires])
+ *     * accessToken is a string
+ *     * expires is an optional expire time in milliseconds
+ * If provisionCallback is used, then Nodemailer does not try to attempt generating the token by itself
+ *
+ * @constructor
+ * @param {Object} options Client information for token generation
+ * @param {String} options.user User e-mail address
+ * @param {String} options.clientId Client ID value
+ * @param {String} options.clientSecret Client secret value
+ * @param {String} options.refreshToken Refresh token for an user
+ * @param {String} options.accessUrl Endpoint for token generation, defaults to 'https://accounts.google.com/o/oauth2/token'
+ * @param {String} options.accessToken An existing valid accessToken
+ * @param {String} options.privateKey Private key for JSW
+ * @param {Number} options.expires Optional Access Token expire time in ms
+ * @param {Number} options.timeout Optional TTL for Access Token in seconds
+ * @param {Function} options.provisionCallback Function to run when a new access token is required
+ */
+class XOAuth2 extends Stream {
+    constructor(options, logger) {
+        super();
+
+        this.options = options || {};
+
+        if (options && options.serviceClient) {
+            if (!options.privateKey || !options.user) {
+                return setImmediate(() => this.emit('error', new Error('Options "privateKey" and "user" are required for service account!')));
+            }
+
+            let serviceRequestTimeout = Math.min(Math.max(Number(this.options.serviceRequestTimeout) || 0, 0), 3600);
+            this.options.serviceRequestTimeout = serviceRequestTimeout || 5 * 60;
+        }
+
+        this.logger = shared.getLogger({
+            logger
+        }, {
+            component: this.options.component || 'OAuth2'
+        });
+
+        this.provisionCallback = typeof this.options.provisionCallback === 'function' ? this.options.provisionCallback : false;
+
+        this.options.accessUrl = this.options.accessUrl || 'https://accounts.google.com/o/oauth2/token';
+        this.options.customHeaders = this.options.customHeaders || {};
+        this.options.customParams = this.options.customParams || {};
+
+        this.accessToken = this.options.accessToken || false;
+
+        if (this.options.expires && Number(this.options.expires)) {
+            this.expires = this.options.expires;
+        } else {
+            let timeout = Math.max(Number(this.options.timeout) || 0, 0);
+            this.expires = timeout && (Date.now() + timeout * 1000) || 0;
+        }
+    }
+
+    /**
+     * Returns or generates (if previous has expired) a XOAuth2 token
+     *
+     * @param {Boolean} renew If false then use cached access token (if available)
+     * @param {Function} callback Callback function with error object and token string
+     */
+    getToken(renew, callback) {
+        if (!renew && this.accessToken && (!this.expires || this.expires > Date.now())) {
+            return callback(null, this.accessToken);
+        }
+
+        let generateCallback = (...args) => {
+            if (args[0]) {
+                this.logger.error({
+                    err: args[0],
+                    tnx: 'OAUTH2',
+                    user: this.options.user,
+                    action: 'renew'
+                }, 'Failed generating new Access Token for %s', this.options.user);
+            } else {
+                this.logger.info({
+                    tnx: 'OAUTH2',
+                    user: this.options.user,
+                    action: 'renew'
+                }, 'Generated new Access Token for %s', this.options.user);
+            }
+            callback(...args);
+        };
+
+        if (this.provisionCallback) {
+            this.provisionCallback(this.options.user, !!renew, (err, accessToken, expires) => {
+                if (!err && accessToken) {
+                    this.accessToken = accessToken;
+                    this.expires = expires || 0;
+                }
+                generateCallback(err, accessToken);
+            });
+        } else {
+            this.generateToken(generateCallback);
+        }
+    }
+
+    /**
+     * Updates token values
+     *
+     * @param {String} accessToken New access token
+     * @param {Number} timeout Access token lifetime in seconds
+     *
+     * Emits 'token': { user: User email-address, accessToken: the new accessToken, timeout: TTL in seconds}
+     */
+    updateToken(accessToken, timeout) {
+        this.accessToken = accessToken;
+        timeout = Math.max(Number(timeout) || 0, 0);
+        this.expires = timeout && Date.now() + timeout * 1000 || 0;
+
+        this.emit('token', {
+            user: this.options.user,
+            accessToken: accessToken || '',
+            expires: this.expires
+        });
+    }
+
+    /**
+     * Generates a new XOAuth2 token with the credentials provided at initialization
+     *
+     * @param {Function} callback Callback function with error object and token string
+     */
+    generateToken(callback) {
+        let urlOptions;
+        if (this.options.serviceClient) {
+            // service account - https://developers.google.com/identity/protocols/OAuth2ServiceAccount
+            let iat = Math.floor(Date.now() / 1000); // unix time
+            let token = this.jwtSignRS256({
+                iss: this.options.serviceClient,
+                scope: this.options.scope || 'https://mail.google.com/',
+                sub: this.options.user,
+                aud: this.options.accessUrl,
+                iat,
+                exp: iat + this.options.serviceRequestTimeout
+            });
+
+            urlOptions = {
+                grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
+                assertion: token
+            };
+
+        } else {
+
+            if (!this.options.refreshToken) {
+                return callback(new Error('Can\'t create new access token for user'));
+            }
+
+            // web app - https://developers.google.com/identity/protocols/OAuth2WebServer
+            urlOptions = {
+                client_id: this.options.clientId || '',
+                client_secret: this.options.clientSecret || '',
+                refresh_token: this.options.refreshToken,
+                grant_type: 'refresh_token'
+            };
+        }
+
+        Object.keys(this.options.customParams).forEach(key => {
+            urlOptions[key] = this.options.customParams[key];
+        });
+
+        this.postRequest(this.options.accessUrl, urlOptions, this.options, (error, body) => {
+            let data;
+
+            if (error) {
+                return callback(error);
+            }
+
+            try {
+                data = JSON.parse(body.toString());
+            } catch (E) {
+                return callback(E);
+            }
+
+            if (!data || typeof data !== 'object') {
+                return callback(new Error('Invalid authentication response'));
+            }
+
+            if (data.error) {
+                return callback(new Error(data.error));
+            }
+
+            if (data.access_token) {
+                this.updateToken(data.access_token, data.expires_in);
+                return callback(null, this.accessToken);
+            }
+
+            return callback(new Error('No access token'));
+        });
+    }
+
+    /**
+     * Converts an access_token and user id into a base64 encoded XOAuth2 token
+     *
+     * @param {String} [accessToken] Access token string
+     * @return {String} Base64 encoded token for IMAP or SMTP login
+     */
+    buildXOAuth2Token(accessToken) {
+        let authData = [
+            'user=' + (this.options.user || ''),
+            'auth=Bearer ' + (accessToken || this.accessToken),
+            '',
+            ''
+        ];
+        return new Buffer(authData.join('\x01'), 'utf-8').toString('base64');
+    }
+
+    /**
+     * Custom POST request handler.
+     * This is only needed to keep paths short in Windows – usually this module
+     * is a dependency of a dependency and if it tries to require something
+     * like the request module the paths get way too long to handle for Windows.
+     * As we do only a simple POST request we do not actually require complicated
+     * logic support (no redirects, no nothing) anyway.
+     *
+     * @param {String} url Url to POST to
+     * @param {String|Buffer} payload Payload to POST
+     * @param {Function} callback Callback function with (err, buff)
+     */
+    postRequest(url, payload, params, callback) {
+        let returned = false;
+
+        let chunks = [];
+        let chunklen = 0;
+
+        let req = fetch(url, {
+            method: 'post',
+            headers: params.customHeaders,
+            body: payload
+        });
+
+        req.on('readable', () => {
+            let chunk;
+            while ((chunk = req.read()) !== null) {
+                chunks.push(chunk);
+                chunklen += chunk.length;
+            }
+        });
+
+        req.once('error', err => {
+            if (returned) {
+                return;
+            }
+            returned = true;
+            return callback(err);
+        });
+
+        req.once('end', () => {
+            if (returned) {
+                return;
+            }
+            returned = true;
+            return callback(null, Buffer.concat(chunks, chunklen));
+        });
+    }
+
+    /**
+     * Encodes a buffer or a string into Base64url format
+     *
+     * @param {Buffer|String} data The data to convert
+     * @return {String} The encoded string
+     */
+    toBase64URL(data) {
+        if (typeof data === 'string') {
+            data = new Buffer(data);
+        }
+
+        return data.toString('base64').
+        replace(/=+/g, ''). // remove '='s
+        replace(/\+/g, '-'). // '+' → '-'
+        replace(/\//g, '_'); // '/' → '_'
+    }
+
+    /**
+     * Creates a JSON Web Token signed with RS256 (SHA256 + RSA)
+     *
+     * @param {Object} payload The payload to include in the generated token
+     * @return {String} The generated and signed token
+     */
+    jwtSignRS256(payload) {
+        payload = [
+            '{"alg":"RS256","typ":"JWT"}',
+            JSON.stringify(payload)
+        ].map(val => this.toBase64URL(val)).join('.');
+        let signature = crypto.createSign('RSA-SHA256').update(payload).sign(this.options.privateKey);
+        return payload + '.' + this.toBase64URL(signature);
+    }
+}
+
+module.exports = XOAuth2;
diff --git a/node_modules/nodemailer/package.json b/node_modules/nodemailer/package.json
new file mode 100644
index 0000000..2df72ec
--- /dev/null
+++ b/node_modules/nodemailer/package.json
@@ -0,0 +1,107 @@
+{
+  "_args": [
+    [
+      {
+        "raw": "nodemailer",
+        "scope": null,
+        "escapedName": "nodemailer",
+        "name": "nodemailer",
+        "rawSpec": "",
+        "spec": "latest",
+        "type": "tag"
+      },
+      "/Users/fzy/project/koa2_Sequelize_project"
+    ]
+  ],
+  "_from": "nodemailer@latest",
+  "_id": "nodemailer@4.0.1",
+  "_inCache": true,
+  "_location": "/nodemailer",
+  "_nodeVersion": "6.10.0",
+  "_npmOperationalInternal": {
+    "host": "packages-18-east.internal.npmjs.com",
+    "tmp": "tmp/nodemailer-4.0.1.tgz_1492078705942_0.3458673018030822"
+  },
+  "_npmUser": {
+    "name": "andris",
+    "email": "andris@kreata.ee"
+  },
+  "_npmVersion": "3.10.10",
+  "_phantomChildren": {},
+  "_requested": {
+    "raw": "nodemailer",
+    "scope": null,
+    "escapedName": "nodemailer",
+    "name": "nodemailer",
+    "rawSpec": "",
+    "spec": "latest",
+    "type": "tag"
+  },
+  "_requiredBy": [
+    "#USER",
+    "/"
+  ],
+  "_resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.0.1.tgz",
+  "_shasum": "b95864b07facee8287e8232effd6f1d56ec75ab2",
+  "_shrinkwrap": null,
+  "_spec": "nodemailer",
+  "_where": "/Users/fzy/project/koa2_Sequelize_project",
+  "author": {
+    "name": "Andris Reinman"
+  },
+  "bugs": {
+    "url": "https://github.com/nodemailer/nodemailer/issues"
+  },
+  "dependencies": {},
+  "description": "Easy as cake e-mail sending from your Node.js applications",
+  "devDependencies": {
+    "bunyan": "^1.8.10",
+    "chai": "^3.5.0",
+    "eslint-config-nodemailer": "^1.0.0",
+    "grunt": "^1.0.1",
+    "grunt-cli": "^1.2.0",
+    "grunt-eslint": "^19.0.0",
+    "grunt-mocha-test": "^0.13.2",
+    "libbase64": "^0.1.0",
+    "libmime": "^3.1.0",
+    "libqp": "^1.1.0",
+    "mocha": "^3.2.0",
+    "proxy": "^0.2.4",
+    "proxy-test-server": "^1.0.0",
+    "sinon": "^2.1.0",
+    "smtp-server": "^3.0.1"
+  },
+  "directories": {},
+  "dist": {
+    "shasum": "b95864b07facee8287e8232effd6f1d56ec75ab2",
+    "tarball": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.0.1.tgz"
+  },
+  "engines": {
+    "node": ">=6.0.0"
+  },
+  "gitHead": "8b5990c5af6b3a60fe3cef4c0492e62d68f4b9ea",
+  "homepage": "https://nodemailer.com/",
+  "keywords": [
+    "Nodemailer"
+  ],
+  "license": "MIT",
+  "main": "lib/nodemailer.js",
+  "maintainers": [
+    {
+      "name": "andris",
+      "email": "andris@node.ee"
+    }
+  ],
+  "name": "nodemailer",
+  "optionalDependencies": {},
+  "readme": "# Nodemailer\n\n![Nodemailer](https://raw.githubusercontent.com/nodemailer/nodemailer/master/assets/nm_logo_200x136.png)\n\nSend e-mails from Node.js – easy as cake! 🍰✉️\n\n<a href=\"https://gitter.im/nodemailer/nodemailer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge\"><img src=\"https://badges.gitter.im/Join Chat.svg\" alt=\"Gitter chat\" height=\"18\"></a> <a href=\"http://travis-ci.org/nodemailer/nodemailer\"><img src=\"https://secure.travis-ci.org/nodemailer/nodemailer.svg\" alt=\"Build Status\" height=\"18\"></a> <a href=\"http://badge.fury.io/js/nodemailer\"><img src=\"https://badge.fury.io/js/nodemailer.svg\" alt=\"NPM version\" height=\"18\"></a> <a href=\"https://www.npmjs.com/package/nodemailer\"><img src=\"https://img.shields.io/npm/dt/nodemailer.svg\" alt=\"NPM downloads\" height=\"18\"></a>\n\n[![NPM](https://nodei.co/npm/nodemailer.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/nodemailer/)\n\nSee [nodemailer.com](https://nodemailer.com/) for documentation and terms.\n\n-------\n\nNodemailer v4.0.0 and up is licensed under the [MIT license](./LICENSE)\n",
+  "readmeFilename": "README.md",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/nodemailer/nodemailer.git"
+  },
+  "scripts": {
+    "test": "grunt"
+  },
+  "version": "4.0.1"
+}
diff --git a/node_modules/smtp-connection/.eslintrc.js b/node_modules/smtp-connection/.eslintrc.js
new file mode 100644
index 0000000..a8a4087
--- /dev/null
+++ b/node_modules/smtp-connection/.eslintrc.js
@@ -0,0 +1,56 @@
+'use strict';
+
+module.exports = {
+    rules: {
+        indent: [2, 4, {
+            SwitchCase: 1
+        }],
+        quotes: [2, 'single'],
+        'linebreak-style': [2, 'unix'],
+        semi: [2, 'always'],
+        strict: [2, 'global'],
+        eqeqeq: 2,
+        'dot-notation': 2,
+        curly: 2,
+        'no-fallthrough': 2,
+        'quote-props': [2, 'as-needed'],
+        'no-unused-expressions': [2, {
+            allowShortCircuit: true
+        }],
+        'no-unused-vars': 2,
+        'no-undef': 2,
+        'handle-callback-err': 2,
+        'no-new': 2,
+        'new-cap': 2,
+        'no-eval': 2,
+        'no-invalid-this': 2,
+        radix: [2, 'always'],
+        'no-use-before-define': [2, 'nofunc'],
+        'callback-return': [2, ['callback', 'cb', 'done']],
+        'comma-dangle': [2, 'never'],
+        'comma-style': [2, 'last'],
+        'no-regex-spaces': 2,
+        'no-empty': 2,
+        'no-duplicate-case': 2,
+        'no-empty-character-class': 2,
+        'no-redeclare': [2, {
+            builtinGlobals: true
+        }],
+        'block-scoped-var': 2,
+        'no-sequences': 2,
+        'no-throw-literal': 2,
+        'no-useless-concat': 2,
+        'no-void': 2,
+        yoda: 2,
+        'no-bitwise': 2,
+        'no-lonely-if': 2,
+        'no-mixed-spaces-and-tabs': 2,
+        'no-console': 2
+    },
+    env: {
+        es6: false,
+        node: true
+    },
+    extends: 'eslint:recommended',
+    fix: true
+};
diff --git a/node_modules/smtp-connection/.npmignore b/node_modules/smtp-connection/.npmignore
new file mode 100644
index 0000000..8a9ec42
--- /dev/null
+++ b/node_modules/smtp-connection/.npmignore
@@ -0,0 +1,3 @@
+.travis.yml
+test
+examples
diff --git a/node_modules/smtp-connection/CHANGELOG.md b/node_modules/smtp-connection/CHANGELOG.md
new file mode 100644
index 0000000..e458704
--- /dev/null
+++ b/node_modules/smtp-connection/CHANGELOG.md
@@ -0,0 +1,164 @@
+# Changelog
+
+## v2.12.0 2016-09-05
+
+  * Updated dependencies
+
+## v2.11.0 2016-08-04
+
+  * Added new envelope option `size` to skip sending messages that are too large
+
+## v2.10.0 2016-07-22
+
+  * Added new option `opportunisticTLS` to allow continuing if STARTTLS failed
+
+## v2.9.0 2016-07-13
+
+  * Added `reset(cb)` method to call `RSET` command
+  * Include failed recipients in the response error object
+
+## v2.8.0 2016-07-07
+
+  * Added full LMTP support. Set `lmtp` option to `true` to switch into LMTP mode
+  * Updated default timeout values
+
+## v2.7.0 2016-07-06
+
+  * Use PIPELINING for multiple RCPT TO if available
+
+## v2.6.0 2016-07-06
+
+  * Added support for DSN
+  * Added new option use8BitMime to indicate that the message might include non-ascii bytes
+  * Added new info property rejectedErrors that includes errors for failed recipients
+  * Updated errors to indicate where the error happened (SMTP command, API, CONN)
+
+## v2.5.0 2016-05-11
+
+  * Bumped dependencies
+
+## v2.4.0 2016-04-24
+
+  * Added experimental support for NTLM authentication
+
+## v2.3.2 2016-04-11
+
+  * Declare SMTPUTF8 usage if an address includes Unicode characters and the server indicates support for it. Fixes an issue with internationalized email addresses that were rejected by Gmail
+
+## v2.3.1 2016-02-20
+
+  * Fix broken requireTLS option
+
+## v2.3.0 2016-02-17
+
+  * Do not modify provided options object
+
+## v2.2.6 2016-02-16
+
+  * Added yet another socket.resume to fixed an issue with proxied sockets and TLS
+
+## v2.2.5 2016-02-15
+
+  * Fixed an issue with proxied sockets and TLS
+
+## v2.2.4 2016-02-11
+
+  * Catch errors that happen while creating a socket
+
+## v2.2.3 2016-02-11
+
+  * Fixed error code for STARTTLS errors
+
+## v2.2.2 2016-02-09
+
+  * Bumped nodemailer-shared
+
+## v2.2.1 2016-02-09
+
+  * Make sure socket is resumed once 'data' handler is set
+
+## v2.2.0 2016-02-08
+
+  * Added new option `secured` to indicate if socket provided by `connection` is already upgraded or not
+
+## v2.1.0 2016-01-30
+
+  * Added new option `connection` to provide an already connected plaintext socket. Useful when behind proxy.
+
+## v2.0.1 2016-01-04
+
+  * Bumped nodemailer-shared
+
+## v2.0.0 2016-01-04
+
+  * Locked dependency version
+
+## v2.0.0-beta.5 2016-01-03
+
+  * Fixed a bug where errors might been thrown before a handler was set
+
+## v2.0.0-beta.3 2016-01-03
+
+  * Use shared function to create the logger instance
+
+## v2.0.0-beta.2 2016-01-03
+
+  * Updated logging. Log information about transmitted message size in bytes
+
+## v2.0.0-beta.1 2016-01-03
+
+  * Re-added `debug` option. If set to true, then logs SMTP traffic, otherwise only transaction events
+  * Pass streamed message content to the logger
+
+## v2.0.0-beta.0 2016-01-02
+
+  * Replaced jshint with eslint
+  * Handle message stream errors
+  * Use bunyan compatible logger interface instead of emitting 'log' events
+
+## v1.3.8 2015-12-29
+
+  * Do not use strict isEmail function, just check that there are no newlines in addresses. Fixes a regression with lax e-mail addresses.
+
+## v1.3.7 2015-12-22
+
+  * Fixed an issue with Node v0.10 where too many events were cleared
+
+## v1.3.6 2015-12-19
+
+  * Updated isemail configuration to only allow SMTP compatible e-mail addresses for the envelope (otherwise valid addresses might include symbols that don't play well with SMTP, eg. line folding inside quoted strings)
+
+## v1.3.5 2015-12-19
+
+  * Validate to and from address to be valid e-mail addresses
+
+## v1.3.2 2015-12-16
+
+  * Added missing 'close' and 'end' event handlers for a STARTTLS-upgraded socket
+
+## v1.3.1 2015-06-30
+
+  * Added partial support for LMTP protocol. Works only with single recipient (does not support multiple responses for DATA command)
+
+## v1.2.0 2015-03-09
+
+  * Connection object has a new property `secure` that indicates if the current connection is using a secure TLS socket or not
+  * Fixed `requireTLS` where the connection was established insecurely if STARTTLS failed, now it returns an error as it should if STARTTLS fails
+
+## v1.1.0 2014-11-11
+
+  * Added additional constructor option `requireTLS` to ensure that the connection is upgraded before any credentials are passed to the server
+  * Added additional constructor option `socket` to use an existing socket instead of creating new one (bantu)
+
+## v1.0.2 2014-10-15
+
+  * Removed CleartextStream.pair.encrypted error handler. Does not seem to be supported by Node v0.11
+
+## v1.0.1 2014-10-15
+
+  * Added 'error' handler for CleartextStream.pair.encrypted object when connecting to TLS.
+
+## v1.0.0 2014-09-26
+
+  * Changed version scheme from 0.x to 1.x.
+  * Improved error handling for timeout on creating a connection. Caused issues with `once('error')` handler as an error might have been emitted twice
diff --git a/node_modules/smtp-connection/Gruntfile.js b/node_modules/smtp-connection/Gruntfile.js
new file mode 100644
index 0000000..77e262b
--- /dev/null
+++ b/node_modules/smtp-connection/Gruntfile.js
@@ -0,0 +1,27 @@
+'use strict';
+
+module.exports = function (grunt) {
+
+    // Project configuration.
+    grunt.initConfig({
+        eslint: {
+            all: ['lib/*.js', 'test/*.js', 'Gruntfile.js']
+        },
+
+        mochaTest: {
+            all: {
+                options: {
+                    reporter: 'spec'
+                },
+                src: ['test/*-test.js']
+            }
+        }
+    });
+
+    // Load the plugin(s)
+    grunt.loadNpmTasks('grunt-eslint');
+    grunt.loadNpmTasks('grunt-mocha-test');
+
+    // Tasks
+    grunt.registerTask('default', ['eslint', 'mochaTest']);
+};
diff --git a/node_modules/smtp-connection/LICENSE b/node_modules/smtp-connection/LICENSE
new file mode 100644
index 0000000..02fccdb
--- /dev/null
+++ b/node_modules/smtp-connection/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2014-2016 Andris Reinman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/node_modules/smtp-connection/README.md b/node_modules/smtp-connection/README.md
new file mode 100644
index 0000000..413cee1
--- /dev/null
+++ b/node_modules/smtp-connection/README.md
@@ -0,0 +1,200 @@
+# smtp-connection
+
+SMTP client module. Connect to SMTP servers and send mail with it.
+
+This module is the successor for the client part of the (now deprecated) SMTP module [simplesmtp](https://www.npmjs.com/package/simplesmtp). For matching SMTP server see [smtp-server](https://www.npmjs.com/package/smtp-server).
+
+[![Build Status](https://secure.travis-ci.org/nodemailer/smtp-connection.svg)](http://travis-ci.org/nodemailer/smtp-connection) [![npm version](https://badge.fury.io/js/smtp-connection.svg)](http://badge.fury.io/js/smtp-connection)
+
+## Usage
+
+Install with npm
+
+```
+npm install smtp-connection
+```
+
+Require in your script
+
+```
+var SMTPConnection = require('smtp-connection');
+```
+
+### Create SMTPConnection instance
+
+```javascript
+var connection = new SMTPConnection(options);
+```
+
+Where
+
+- **options** defines connection data
+
+  - **options.port** is the port to connect to (defaults to 25 or 465)
+  - **options.host** is the hostname or IP address to connect to (defaults to 'localhost')
+  - **options.secure** defines if the connection should use SSL (if `true`) or not (if `false`)
+  - **options.ignoreTLS** turns off STARTTLS support if true
+  - **options.requireTLS** forces the client to use STARTTLS. Returns an error if upgrading the connection is not possible or fails.
+  - **options.opportunisticTLS** tries to use STARTTLS and continues normally if it fails
+  - **options.name** optional hostname of the client, used for identifying to the server
+  - **options.localAddress** is the local interface to bind to for network connections
+  - **options.connectionTimeout** how many milliseconds to wait for the connection to establish
+  - **options.greetingTimeout** how many milliseconds to wait for the greeting after connection is established
+  - **options.socketTimeout** how many milliseconds of inactivity to allow
+  - **options.logger** optional [bunyan](https://github.com/trentm/node-bunyan) compatible logger instance. If set to `true` then logs to console. If value is not set or is `false` then nothing is logged
+  - **options.debug** if set to true, then logs SMTP traffic, otherwise logs only transaction events
+  - **options.authMethod** defines preferred authentication method, e.g. 'PLAIN'
+  - **options.tls** defines additional options to be passed to the socket constructor, e.g. _{rejectUnauthorized: true}_
+  - **options.socket** - initialized socket to use instead of creating a new one
+  - **options.connection** - connected socket to use instead of creating and connecting a new one. If `secure` option is true, then socket is upgraded from plaintext to ciphertext
+
+### Events
+
+SMTPConnection instances are event emitters with the following events
+
+- **'error'** _(err)_ emitted when an error occurs. Connection is closed automatically in this case.
+- **'connect'** emitted when the connection is established
+- **'end'** when the instance is destroyed
+
+### connect
+
+Establish the connection
+
+```javascript
+connection.connect(callback)
+```
+
+Where
+
+- **callback** is the function to run once the connection is established. The function is added as a listener to the 'connect' event.
+
+After the connect event the `connection` has the following properties:
+
+- **connection.secure** - if `true` then the connection uses a TLS socket, otherwise it is using a cleartext socket. Connection can start out as cleartext but if available (or `requireTLS` is set to true) connection upgrade is tried
+
+### login
+
+If the server requires authentication you can login with
+
+```javascript
+connection.login(auth, callback)
+```
+
+Where
+
+- **auth** is the authentication object
+
+  - **auth.user** is the username
+  - **auth.pass** is the password for the user
+  - **auth.xoauth2** is the OAuth2 access token (preferred if both `pass` and `xoauth2` values are set) or an [XOAuth2](https://github.com/andris9/xoauth2) token generator object.
+
+- **callback** is the callback to run once the authentication is finished. Callback has the following arguments
+
+  - **err** and error object if authentication failed
+
+If a [XOAuth2](https://github.com/andris9/xoauth2) token generator is used as the value for `auth.xoauth2` then you do not need to set `auth.user`. XOAuth2 generator generates required accessToken itself if it is missing or expired. In this case if the authentication fails, a new token is requeested and the authentication is retried. If it still fails, an error is returned.
+
+**XOAuth2 Example**
+
+```javascript
+var generator = require('xoauth2').createXOAuth2Generator({
+    user: '{username}',
+    clientId: '{Client ID}',
+    clientSecret: '{Client Secret}',
+    refreshToken: '{refresh-token}'
+});
+
+// listen for token updates
+// you probably want to store these to a db
+generator.on('token', function(token){
+    console.log('New token for %s: %s', token.user, token.accessToken);
+});
+
+// login
+connection.login({
+    xoauth2: generator
+}, callback);
+```
+
+### Login using NTLM
+
+`smtp-connection` has experimental support for NTLM authentication. You can try it out like this:
+
+```javascript
+connection.login({
+    domain: 'windows-domain',
+    workstation: 'windows-workstation',
+    user: 'user@somedomain.com',
+    pass: 'pass'
+}, callback);
+```
+
+I do not have access to an actual server that supports NTLM authentication so this feature is untested and should be used carefully.
+
+### send
+
+Once the connection is authenticated (or just after connection is established if authentication is not required), you can send mail with
+
+```javascript
+connection.send(envelope, message, callback)
+```
+
+Where
+
+- **envelope** is the envelope object to use
+
+  - **envelope.from** is the sender address
+  - **envelope.to** is the recipient address or an array of addresses
+  - **envelope.size** is an optional value of the predicted size of the message in bytes. This value is used if the server supports the SIZE extension (RFC1870)
+  - **envelope.use8BitMime** if `true` then inform the server that this message might contain bytes outside 7bit ascii range
+  - **envelope.dsn** is the dsn options
+
+    - **envelope.dsn.ret** return either the full message 'FULL' or only headers 'HDRS'
+    - **envelope.dsn.envid** sender's 'envelope identifier' for tracking
+    - **envelope.dsn.notify** when to send a DSN. Multiple options are OK - array or comma delimited. NEVER must appear by itself. Available options: 'NEVER', 'SUCCESS', 'FAILURE', 'DELAY'
+    - **envelope.dsn.orcpt** original recipient
+
+- **message** is either a String, Buffer or a Stream. All newlines are converted to \r\n and all dots are escaped automatically, no need to convert anything before.
+
+- **callback** is the callback to run once the sending is finished or failed. Callback has the following arguments
+
+  - **err** and error object if sending failed
+
+    - **code** string code identifying the error, for example 'EAUTH' is returned when authentication fails
+    - **response** is the last response received from the server (if the error is caused by an error response from the server)
+    - **responseCode** is the numeric response code of the `response` string (if available)
+
+  - **info** information object about accepted and rejected recipients
+
+    - **accepted** an array of accepted recipient addresses. Normally this array should contain at least one address except when in LMTP mode. In this case the message itself might have succeeded but all recipients were rejected after sending the message.
+    - **rejected** an array of rejected recipient addresses. This array includes both the addresses that were rejected before sending the message and addresses rejected after sending it if using LMTP
+    - **rejectedErrors** if some recipients were rejected then this property holds an array of error objects for the rejected recipients
+    - **response** is the last response received from the server
+
+### quit
+
+Use it for graceful disconnect
+
+```javascript
+connection.quit();
+```
+
+### close
+
+Use it for less graceful disconnect
+
+```javascript
+connection.close();
+```
+
+### reset
+
+Use it to reset current session (invokes RSET command)
+
+```javascript
+connection.reset(callback);
+```
+
+## License
+
+**MIT**
diff --git a/node_modules/smtp-connection/lib/data-stream.js b/node_modules/smtp-connection/lib/data-stream.js
new file mode 100644
index 0000000..d454dc3
--- /dev/null
+++ b/node_modules/smtp-connection/lib/data-stream.js
@@ -0,0 +1,111 @@
+'use strict';
+
+var stream = require('stream');
+var Transform = stream.Transform;
+var util = require('util');
+
+module.exports = DataStream;
+
+/**
+ * Escapes dots in the beginning of lines. Ends the stream with <CR><LF>.<CR><LF>
+ * Also makes sure that only <CR><LF> sequences are used for linebreaks
+ *
+ * @param {Object} options Stream options
+ */
+function DataStream(options) {
+    // init Transform
+    this.options = options || {};
+    this._curLine = '';
+
+    this.inByteCount = 0;
+    this.outByteCount = 0;
+    this.lastByte = false;
+
+    Transform.call(this, this.options);
+}
+util.inherits(DataStream, Transform);
+
+/**
+ * Escapes dots
+ */
+DataStream.prototype._transform = function (chunk, encoding, done) {
+    var chunks = [];
+    var chunklen = 0;
+    var i, len, lastPos = 0;
+    var buf;
+
+    if (!chunk || !chunk.length) {
+        return done();
+    }
+
+    if (typeof chunk === 'string') {
+        chunk = new Buffer(chunk);
+    }
+
+    this.inByteCount += chunk.length;
+
+    for (i = 0, len = chunk.length; i < len; i++) {
+        if (chunk[i] === 0x2E) { // .
+            if (
+                (i && chunk[i - 1] === 0x0A) ||
+                (!i && (!this.lastByte || this.lastByte === 0x0A))
+            ) {
+                buf = chunk.slice(lastPos, i + 1);
+                chunks.push(buf);
+                chunks.push(new Buffer('.'));
+                chunklen += buf.length + 1;
+                lastPos = i + 1;
+            }
+        } else if (chunk[i] === 0x0A) { // .
+            if (
+                (i && chunk[i - 1] !== 0x0D) ||
+                (!i && this.lastByte !== 0x0D)
+            ) {
+                if (i > lastPos) {
+                    buf = chunk.slice(lastPos, i);
+                    chunks.push(buf);
+                    chunklen += buf.length + 2;
+                } else {
+                    chunklen += 2;
+                }
+                chunks.push(new Buffer('\r\n'));
+                lastPos = i + 1;
+            }
+        }
+    }
+
+    if (chunklen) {
+        // add last piece
+        if (lastPos < chunk.length) {
+            buf = chunk.slice(lastPos);
+            chunks.push(buf);
+            chunklen += buf.length;
+        }
+
+        this.outByteCount += chunklen;
+        this.push(Buffer.concat(chunks, chunklen));
+    } else {
+        this.outByteCount += chunk.length;
+        this.push(chunk);
+    }
+
+    this.lastByte = chunk[chunk.length - 1];
+    done();
+};
+
+/**
+ * Finalizes the stream with a dot on a single line
+ */
+DataStream.prototype._flush = function (done) {
+    var buf;
+    if (this.lastByte === 0x0A) {
+        buf = new Buffer('.\r\n');
+    } else if (this.lastByte === 0x0D) {
+        buf = new Buffer('\n.\r\n');
+    } else {
+        buf = new Buffer('\r\n.\r\n');
+    }
+    this.outByteCount += buf.length;
+    this.push(buf);
+    done();
+};
diff --git a/node_modules/smtp-connection/lib/smtp-connection.js b/node_modules/smtp-connection/lib/smtp-connection.js
new file mode 100644
index 0000000..f4fe419
--- /dev/null
+++ b/node_modules/smtp-connection/lib/smtp-connection.js
@@ -0,0 +1,1443 @@
+'use strict';
+
+var packageInfo = require('../package.json');
+var EventEmitter = require('events').EventEmitter;
+var util = require('util');
+var net = require('net');
+var tls = require('tls');
+var os = require('os');
+var crypto = require('crypto');
+var DataStream = require('./data-stream');
+var PassThrough = require('stream').PassThrough;
+var shared = require('nodemailer-shared');
+var ntlm = require('httpntlm/ntlm');
+
+// default timeout values in ms
+var CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection to be established
+var SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity before disconnecting the client
+var GREETING_TIMEOUT = 30 * 1000; // how much to wait after connection is established but SMTP greeting is not receieved
+
+module.exports = SMTPConnection;
+
+/**
+ * Generates a SMTP connection object
+ *
+ * Optional options object takes the following possible properties:
+ *
+ *  * **port** - is the port to connect to (defaults to 25 or 465)
+ *  * **host** - is the hostname or IP address to connect to (defaults to 'localhost')
+ *  * **secure** - use SSL
+ *  * **ignoreTLS** - ignore server support for STARTTLS
+ *  * **requireTLS** - forces the client to use STARTTLS
+ *  * **name** - the name of the client server
+ *  * **localAddress** - outbound address to bind to (see: http://nodejs.org/api/net.html#net_net_connect_options_connectionlistener)
+ *  * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 10000)
+ *  * **connectionTimeout** - how many milliseconds to wait for the connection to establish
+ *  * **socketTimeout** - Time of inactivity until the connection is closed (defaults to 1 hour)
+ *  * **lmtp** - if true, uses LMTP instead of SMTP protocol
+ *  * **logger** - bunyan compatible logger interface
+ *  * **debug** - if true pass SMTP traffic to the logger
+ *  * **tls** - options for createCredentials
+ *  * **socket** - existing socket to use instead of creating a new one (see: http://nodejs.org/api/net.html#net_class_net_socket)
+ *  * **secured** - boolean indicates that the provided socket has already been upgraded to tls
+ *
+ * @constructor
+ * @namespace SMTP Client module
+ * @param {Object} [options] Option properties
+ */
+function SMTPConnection(options) {
+    EventEmitter.call(this);
+
+    this.id = crypto.randomBytes(8).toString('base64').replace(/\W/g, '');
+    this.stage = 'init';
+
+    this.options = options || {};
+
+    this.secureConnection = !!this.options.secure;
+    this.alreadySecured = !!this.options.secured;
+
+    this.port = this.options.port || (this.secureConnection ? 465 : 25);
+    this.host = this.options.host || 'localhost';
+
+    if (typeof this.options.secure === 'undefined' && this.port === 465) {
+        // if secure option is not set but port is 465, then default to secure
+        this.secureConnection = true;
+    }
+
+    this.name = this.options.name || this._getHostname();
+
+    this.logger = shared.getLogger(this.options);
+
+    /**
+     * Expose version nr, just for the reference
+     * @type {String}
+     */
+    this.version = packageInfo.version;
+
+    /**
+     * If true, then the user is authenticated
+     * @type {Boolean}
+     */
+    this.authenticated = false;
+
+    /**
+     * If set to true, this instance is no longer active
+     * @private
+     */
+    this.destroyed = false;
+
+    /**
+     * Defines if the current connection is secure or not. If not,
+     * STARTTLS can be used if available
+     * @private
+     */
+    this.secure = !!this.secureConnection;
+
+    /**
+     * Store incomplete messages coming from the server
+     * @private
+     */
+    this._remainder = '';
+
+    /**
+     * Unprocessed responses from the server
+     * @type {Array}
+     */
+    this._responseQueue = [];
+
+    /**
+     * The socket connecting to the server
+     * @publick
+     */
+    this._socket = false;
+
+    /**
+     * Lists supported auth mechanisms
+     * @private
+     */
+    this._supportedAuth = [];
+
+    /**
+     * Includes current envelope (from, to)
+     * @private
+     */
+    this._envelope = false;
+
+    /**
+     * Lists supported extensions
+     * @private
+     */
+    this._supportedExtensions = [];
+
+    /**
+     * Defines the maximum allowed size for a single message
+     * @private
+     */
+    this._maxAllowedSize = 0;
+
+    /**
+     * Function queue to run if a data chunk comes from the server
+     * @private
+     */
+    this._responseActions = [];
+    this._recipientQueue = [];
+
+    /**
+     * Timeout variable for waiting the greeting
+     * @private
+     */
+    this._greetingTimeout = false;
+
+    /**
+     * Timeout variable for waiting the connection to start
+     * @private
+     */
+    this._connectionTimeout = false;
+
+    /**
+     * If the socket is deemed already closed
+     * @private
+     */
+    this._destroyed = false;
+
+    /**
+     * If the socket is already being closed
+     * @private
+     */
+    this._closing = false;
+}
+util.inherits(SMTPConnection, EventEmitter);
+
+/**
+ * Creates a connection to a SMTP server and sets up connection
+ * listener
+ */
+SMTPConnection.prototype.connect = function (connectCallback) {
+    if (typeof connectCallback === 'function') {
+        this.once('connect', function () {
+            this.logger.debug('[%s] SMTP handshake finished', this.id);
+            connectCallback();
+        }.bind(this));
+    }
+
+    var opts = {
+        port: this.port,
+        host: this.host
+    };
+
+    if (this.options.localAddress) {
+        opts.localAddress = this.options.localAddress;
+    }
+
+    if (this.options.connection) {
+        // connection is already opened
+        this._socket = this.options.connection;
+        if (this.secureConnection && !this.alreadySecured) {
+            setImmediate(this._upgradeConnection.bind(this, function (err) {
+                if (err) {
+                    this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'CONN');
+                    return;
+                }
+                this._onConnect();
+            }.bind(this)));
+        } else {
+            setImmediate(this._onConnect.bind(this));
+        }
+    } else if (this.options.socket) {
+        // socket object is set up but not yet connected
+        this._socket = this.options.socket;
+        try {
+            this._socket.connect(this.port, this.host, this._onConnect.bind(this));
+        } catch (E) {
+            return setImmediate(this._onError.bind(this, E, 'ECONNECTION', false, 'CONN'));
+        }
+    } else if (this.secureConnection) {
+        // connect using tls
+        if (this.options.tls) {
+            Object.keys(this.options.tls).forEach(function (key) {
+                opts[key] = this.options.tls[key];
+            }.bind(this));
+        }
+        try {
+            this._socket = tls.connect(this.port, this.host, opts, this._onConnect.bind(this));
+        } catch (E) {
+            return setImmediate(this._onError.bind(this, E, 'ECONNECTION', false, 'CONN'));
+        }
+    } else {
+        // connect using plaintext
+        try {
+            this._socket = net.connect(opts, this._onConnect.bind(this));
+        } catch (E) {
+            return setImmediate(this._onError.bind(this, E, 'ECONNECTION', false, 'CONN'));
+        }
+    }
+
+    this._connectionTimeout = setTimeout(function () {
+        this._onError('Connection timeout', 'ETIMEDOUT', false, 'CONN');
+    }.bind(this), this.options.connectionTimeout || CONNECTION_TIMEOUT);
+
+    this._socket.on('error', function (err) {
+        this._onError(err, 'ECONNECTION', false, 'CONN');
+    }.bind(this));
+};
+
+/**
+ * Sends QUIT
+ */
+SMTPConnection.prototype.quit = function () {
+    this._sendCommand('QUIT');
+    this._responseActions.push(this.close);
+};
+
+/**
+ * Closes the connection to the server
+ */
+SMTPConnection.prototype.close = function () {
+    clearTimeout(this._connectionTimeout);
+    clearTimeout(this._greetingTimeout);
+    this._responseActions = [];
+
+    // allow to run this function only once
+    if (this._closing) {
+        return;
+    }
+    this._closing = true;
+
+    var closeMethod = 'end';
+
+    if (this.stage === 'init') {
+        // Close the socket immediately when connection timed out
+        closeMethod = 'destroy';
+    }
+
+    this.logger.debug('[%s] Closing connection to the server using "%s"', this.id, closeMethod);
+
+    var socket = this._socket && this._socket.socket || this._socket;
+
+    if (socket && !socket.destroyed) {
+        try {
+            this._socket[closeMethod]();
+        } catch (E) {
+            // just ignore
+        }
+    }
+
+    this._destroy();
+};
+
+/**
+ * Authenticate user
+ */
+SMTPConnection.prototype.login = function (authData, callback) {
+    this._auth = authData || {};
+    this._user = this._auth.xoauth2 && this._auth.xoauth2.options && this._auth.xoauth2.options.user || this._auth.user || '';
+
+    this._authMethod = false;
+    if (this.options.authMethod) {
+        this._authMethod = this.options.authMethod.toUpperCase().trim();
+    } else if (this._auth.xoauth2 && this._supportedAuth.indexOf('XOAUTH2') >= 0) {
+        this._authMethod = 'XOAUTH2';
+    } else if (this._auth.domain && this._supportedAuth.indexOf('NTLM') >= 0) {
+        this._authMethod = 'NTLM';
+    } else {
+        // use first supported
+        this._authMethod = (this._supportedAuth[0] || 'PLAIN').toUpperCase().trim();
+    }
+
+    switch (this._authMethod) {
+        case 'XOAUTH2':
+            this._handleXOauth2Token(false, callback);
+            return;
+        case 'LOGIN':
+            this._responseActions.push(function (str) {
+                this._actionAUTH_LOGIN_USER(str, callback);
+            }.bind(this));
+            this._sendCommand('AUTH LOGIN');
+            return;
+        case 'PLAIN':
+            this._responseActions.push(function (str) {
+                this._actionAUTHComplete(str, callback);
+            }.bind(this));
+            this._sendCommand('AUTH PLAIN ' + new Buffer(
+                //this._auth.user+'\u0000'+
+                '\u0000' + // skip authorization identity as it causes problems with some servers
+                this._auth.user + '\u0000' +
+                this._auth.pass, 'utf-8').toString('base64'));
+            return;
+        case 'CRAM-MD5':
+            this._responseActions.push(function (str) {
+                this._actionAUTH_CRAM_MD5(str, callback);
+            }.bind(this));
+            this._sendCommand('AUTH CRAM-MD5');
+            return;
+        case 'NTLM':
+            this._responseActions.push(function (str) {
+                this._actionAUTH_NTLM_TYPE1(str, callback);
+            }.bind(this));
+            this._sendCommand('AUTH ' + ntlm.createType1Message({
+                domain: this._auth.domain || '',
+                workstation: this._auth.workstation || ''
+            }));
+            return;
+    }
+
+    return callback(this._formatError('Unknown authentication method "' + this._authMethod + '"', 'EAUTH', false, 'API'));
+};
+
+/**
+ * Sends a message
+ *
+ * @param {Object} envelope Envelope object, {from: addr, to: [addr]}
+ * @param {Object} message String, Buffer or a Stream
+ * @param {Function} callback Callback to return once sending is completed
+ */
+SMTPConnection.prototype.send = function (envelope, message, done) {
+    if (!message) {
+        return done(this._formatError('Empty message', 'EMESSAGE', false, 'API'));
+    }
+
+    // reject larger messages than allowed
+    if (this._maxAllowedSize && envelope.size > this._maxAllowedSize) {
+        return setImmediate(function () {
+            done(this._formatError('Message size larger than allowed ' + this._maxAllowedSize, 'EMESSAGE', false, 'MAIL FROM'));
+        }.bind(this));
+    }
+
+    // ensure that callback is only called once
+    var returned = false;
+    var callback = function () {
+        if (returned) {
+            return;
+        }
+        returned = true;
+
+        done.apply(null, Array.prototype.slice.call(arguments));
+    };
+
+    if (typeof message.on === 'function') {
+        message.on('error', function (err) {
+            return callback(this._formatError(err, 'ESTREAM', false, 'API'));
+        }.bind(this));
+    }
+
+    this._setEnvelope(envelope, function (err, info) {
+        if (err) {
+            return callback(err);
+        }
+        var stream = this._createSendStream(function (err, str) {
+            if (err) {
+                return callback(err);
+            }
+            info.response = str;
+            return callback(null, info);
+        });
+        if (typeof message.pipe === 'function') {
+            message.pipe(stream);
+        } else {
+            stream.write(message);
+            stream.end();
+        }
+
+    }.bind(this));
+};
+
+/**
+ * Resets connection state
+ *
+ * @param {Function} callback Callback to return once connection is reset
+ */
+SMTPConnection.prototype.reset = function (callback) {
+    this._sendCommand('RSET');
+    this._responseActions.push(function (str) {
+        if (str.charAt(0) !== '2') {
+            return callback(this._formatError('Could not reset session state:\n' + str, 'EPROTOCOL', str, 'RSET'));
+        }
+        this._envelope = false;
+        return callback(null, true);
+    }.bind(this));
+};
+
+/**
+ * Connection listener that is run when the connection to
+ * the server is opened
+ *
+ * @event
+ */
+SMTPConnection.prototype._onConnect = function () {
+    clearTimeout(this._connectionTimeout);
+
+    this.logger.info('[%s] %s established to %s:%s', this.id, this.secure ? 'Secure connection' : 'Connection', this._socket.remoteAddress, this._socket.remotePort);
+
+    if (this._destroyed) {
+        // Connection was established after we already had canceled it
+        this.close();
+        return;
+    }
+
+    this.stage = 'connected';
+
+    // clear existing listeners for the socket
+    this._socket.removeAllListeners('data');
+    this._socket.removeAllListeners('timeout');
+    this._socket.removeAllListeners('close');
+    this._socket.removeAllListeners('end');
+
+    this._socket.on('data', this._onData.bind(this));
+    this._socket.once('close', this._onClose.bind(this));
+    this._socket.once('end', this._onEnd.bind(this));
+
+    this._socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT);
+    this._socket.on('timeout', this._onTimeout.bind(this));
+
+    this._greetingTimeout = setTimeout(function () {
+        // if still waiting for greeting, give up
+        if (this._socket && !this._destroyed && this._responseActions[0] === this._actionGreeting) {
+            this._onError('Greeting never received', 'ETIMEDOUT', false, 'CONN');
+        }
+    }.bind(this), this.options.greetingTimeout || GREETING_TIMEOUT);
+
+    this._responseActions.push(this._actionGreeting);
+
+    // we have a 'data' listener set up so resume socket if it was paused
+    this._socket.resume();
+};
+
+/**
+ * 'data' listener for data coming from the server
+ *
+ * @event
+ * @param {Buffer} chunk Data chunk coming from the server
+ */
+SMTPConnection.prototype._onData = function (chunk) {
+    if (this._destroyed || !chunk || !chunk.length) {
+        return;
+    }
+
+    var data = (chunk || '').toString('binary');
+    var lines = (this._remainder + data).split(/\r?\n/);
+    var lastline;
+
+    this._remainder = lines.pop();
+
+    for (var i = 0, len = lines.length; i < len; i++) {
+        if (this._responseQueue.length) {
+            lastline = this._responseQueue[this._responseQueue.length - 1];
+            if (/^\d+\-/.test(lastline.split('\n').pop())) {
+                this._responseQueue[this._responseQueue.length - 1] += '\n' + lines[i];
+                continue;
+            }
+        }
+        this._responseQueue.push(lines[i]);
+    }
+
+    this._processResponse();
+};
+
+/**
+ * 'error' listener for the socket
+ *
+ * @event
+ * @param {Error} err Error object
+ * @param {String} type Error name
+ */
+SMTPConnection.prototype._onError = function (err, type, data, command) {
+    clearTimeout(this._connectionTimeout);
+    clearTimeout(this._greetingTimeout);
+
+    if (this._destroyed) {
+        // just ignore, already closed
+        // this might happen when a socket is canceled because of reached timeout
+        // but the socket timeout error itself receives only after
+        return;
+    }
+
+    err = this._formatError(err, type, data, command);
+
+    this.logger.error('[%s] %s', this.id, err.message);
+
+    this.emit('error', err);
+    this.close();
+};
+
+SMTPConnection.prototype._formatError = function (message, type, response, command) {
+    var err;
+
+    if (/Error\]$/i.test(Object.prototype.toString.call(message))) {
+        err = message;
+    } else {
+        err = new Error(message);
+    }
+
+    if (type && type !== 'Error') {
+        err.code = type;
+    }
+
+    if (response) {
+        err.response = response;
+        err.message += ': ' + response;
+    }
+
+    var responseCode = typeof response === 'string' && Number((response.match(/^\d+/) || [])[0]) || false;
+    if (responseCode) {
+        err.responseCode = responseCode;
+    }
+
+    if (command) {
+        err.command = command;
+    }
+
+    return err;
+};
+
+/**
+ * 'close' listener for the socket
+ *
+ * @event
+ */
+SMTPConnection.prototype._onClose = function () {
+    this.logger.info('[%s] Connection closed', this.id);
+
+    if ([this._actionGreeting, this.close].indexOf(this._responseActions[0]) < 0 && !this._destroyed) {
+        return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', false, 'CONN');
+    }
+
+    this._destroy();
+};
+
+/**
+ * 'end' listener for the socket
+ *
+ * @event
+ */
+SMTPConnection.prototype._onEnd = function () {
+    this._destroy();
+};
+
+/**
+ * 'timeout' listener for the socket
+ *
+ * @event
+ */
+SMTPConnection.prototype._onTimeout = function () {
+    return this._onError(new Error('Timeout'), 'ETIMEDOUT', false, 'CONN');
+};
+
+/**
+ * Destroys the client, emits 'end'
+ */
+SMTPConnection.prototype._destroy = function () {
+    if (this._destroyed) {
+        return;
+    }
+    this._destroyed = true;
+    this.emit('end');
+};
+
+/**
+ * Upgrades the connection to TLS
+ *
+ * @param {Function} callback Callback function to run when the connection
+ *        has been secured
+ */
+SMTPConnection.prototype._upgradeConnection = function (callback) {
+    // do not remove all listeners or it breaks node v0.10 as there's
+    // apparently a 'finish' event set that would be cleared as well
+
+    // we can safely keep 'error', 'end', 'close' etc. events
+    this._socket.removeAllListeners('data'); // incoming data is going to be gibberish from this point onwards
+    this._socket.removeAllListeners('timeout'); // timeout will be re-set for the new socket object
+
+    var socketPlain = this._socket;
+    var opts = {
+        socket: this._socket,
+        host: this.host
+    };
+
+    Object.keys(this.options.tls || {}).forEach(function (key) {
+        opts[key] = this.options.tls[key];
+    }.bind(this));
+
+    this._socket = tls.connect(opts, function () {
+        this.secure = true;
+        this._socket.on('data', this._onData.bind(this));
+
+        socketPlain.removeAllListeners('close');
+        socketPlain.removeAllListeners('end');
+
+        return callback(null, true);
+    }.bind(this));
+
+    this._socket.on('error', this._onError.bind(this));
+    this._socket.once('close', this._onClose.bind(this));
+    this._socket.once('end', this._onEnd.bind(this));
+
+    this._socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT); // 10 min.
+    this._socket.on('timeout', this._onTimeout.bind(this));
+
+    // resume in case the socket was paused
+    socketPlain.resume();
+};
+
+/**
+ * Processes queued responses from the server
+ *
+ * @param {Boolean} force If true, ignores _processing flag
+ */
+SMTPConnection.prototype._processResponse = function () {
+    if (!this._responseQueue.length) {
+        return false;
+    }
+
+    var str = (this._responseQueue.shift() || '').toString();
+
+    if (/^\d+\-/.test(str.split('\n').pop())) {
+        // keep waiting for the final part of multiline response
+        return;
+    }
+
+    if (this.options.debug) {
+        this.logger.debug('[%s] S: %s', this.id, str.replace(/\r?\n$/, ''));
+    }
+
+    if (!str.trim()) { // skip unexpected empty lines
+        setImmediate(this._processResponse.bind(this, true));
+    }
+
+    var action = this._responseActions.shift();
+
+    if (typeof action === 'function') {
+        action.call(this, str);
+        setImmediate(this._processResponse.bind(this, true));
+    } else {
+        return this._onError(new Error('Unexpected Response'), 'EPROTOCOL', str, 'CONN');
+    }
+};
+
+/**
+ * Send a command to the server, append \r\n
+ *
+ * @param {String} str String to be sent to the server
+ */
+SMTPConnection.prototype._sendCommand = function (str) {
+    if (this._destroyed) {
+        // Connection already closed, can't send any more data
+        return;
+    }
+
+    if (this._socket.destroyed) {
+        return this.close();
+    }
+
+    if (this.options.debug) {
+        this.logger.debug('[%s] C: %s', this.id, (str || '').toString().replace(/\r?\n$/, ''));
+    }
+
+    this._socket.write(new Buffer(str + '\r\n', 'utf-8'));
+};
+
+/**
+ * Initiates a new message by submitting envelope data, starting with
+ * MAIL FROM: command
+ *
+ * @param {Object} envelope Envelope object in the form of
+ *        {from:'...', to:['...']}
+ *        or
+ *        {from:{address:'...',name:'...'}, to:[address:'...',name:'...']}
+ */
+SMTPConnection.prototype._setEnvelope = function (envelope, callback) {
+    var args = [];
+    var useSmtpUtf8 = false;
+
+    this._envelope = envelope || {};
+    this._envelope.from = (this._envelope.from && this._envelope.from.address || this._envelope.from || '').toString().trim();
+
+    this._envelope.to = [].concat(this._envelope.to || []).map(function (to) {
+        return (to && to.address || to || '').toString().trim();
+    });
+
+    if (!this._envelope.to.length) {
+        return callback(this._formatError('No recipients defined', 'EENVELOPE', false, 'API'));
+    }
+
+    if (this._envelope.from && /[\r\n<>]/.test(this._envelope.from)) {
+        return callback(this._formatError('Invalid sender ' + JSON.stringify(this._envelope.from), 'EENVELOPE', false, 'API'));
+    }
+
+    // check if the sender address uses only ASCII characters,
+    // otherwise require usage of SMTPUTF8 extension
+    if (/[\x80-\uFFFF]/.test(this._envelope.from)) {
+        useSmtpUtf8 = true;
+    }
+
+    for (var i = 0, len = this._envelope.to.length; i < len; i++) {
+        if (!this._envelope.to[i] || /[\r\n<>]/.test(this._envelope.to[i])) {
+            return callback(this._formatError('Invalid recipient ' + JSON.stringify(this._envelope.to[i]), 'EENVELOPE', false, 'API'));
+        }
+
+        // check if the recipients addresses use only ASCII characters,
+        // otherwise require usage of SMTPUTF8 extension
+        if (/[\x80-\uFFFF]/.test(this._envelope.to[i])) {
+            useSmtpUtf8 = true;
+        }
+    }
+
+    // clone the recipients array for latter manipulation
+    this._envelope.rcptQueue = JSON.parse(JSON.stringify(this._envelope.to || []));
+    this._envelope.rejected = [];
+    this._envelope.rejectedErrors = [];
+    this._envelope.accepted = [];
+
+    if (this._envelope.dsn) {
+        try {
+            this._envelope.dsn = this._setDsnEnvelope(this._envelope.dsn);
+        } catch (err) {
+            return callback(this._formatError('Invalid dsn ' + err.message, 'EENVELOPE', false, 'API'));
+        }
+    }
+
+    this._responseActions.push(function (str) {
+        this._actionMAIL(str, callback);
+    }.bind(this));
+
+    // If the server supports SMTPUTF8 and the envelope includes an internationalized
+    // email address then append SMTPUTF8 keyword to the MAIL FROM command
+    if (useSmtpUtf8 && this._supportedExtensions.indexOf('SMTPUTF8') >= 0) {
+        args.push('SMTPUTF8');
+        this._usingSmtpUtf8 = true;
+    }
+
+    // If the server supports 8BITMIME and the message might contain non-ascii bytes
+    // then append the 8BITMIME keyword to the MAIL FROM command
+    if (this._envelope.use8BitMime && this._supportedExtensions.indexOf('8BITMIME') >= 0) {
+        args.push('BODY=8BITMIME');
+        this._using8BitMime = true;
+    }
+
+    if (this._envelope.size && this._supportedExtensions.indexOf('SIZE') >= 0) {
+        args.push('SIZE=' + this._envelope.size);
+    }
+
+    // If the server supports DSN and the envelope includes an DSN prop
+    // then append DSN params to the MAIL FROM command
+    if (this._envelope.dsn && this._supportedExtensions.indexOf('DSN') >= 0) {
+        if (this._envelope.dsn.ret) {
+            args.push('RET=' + this._envelope.dsn.ret);
+        }
+        if (this._envelope.dsn.envid) {
+            args.push('ENVID=' + this._envelope.dsn.envid);
+        }
+    }
+
+    this._sendCommand('MAIL FROM:<' + (this._envelope.from) + '>' + (args.length ? ' ' + args.join(' ') : ''));
+};
+
+SMTPConnection.prototype._setDsnEnvelope = function (params) {
+    var ret = params.ret ? params.ret.toString().toUpperCase() : null;
+    if (ret && ['FULL', 'HDRS'].indexOf(ret) < 0) {
+        throw new Error('ret: ' + JSON.stringify(ret));
+    }
+    var envid = params.envid ? params.envid.toString() : null;
+    var notify = params.notify ? params.notify : null;
+    if (notify) {
+        if (typeof notify === 'string') {
+            notify = notify.split(',');
+        }
+        notify = notify.map(function (n) {
+            return n.trim().toUpperCase();
+        });
+        var validNotify = ['NEVER', 'SUCCESS', 'FAILURE', 'DELAY'];
+        var invaliNotify = notify.filter(function (n) {
+            return validNotify.indexOf(n) === -1;
+        });
+        if (invaliNotify.length || (notify.length > 1 && notify.indexOf('NEVER') >= 0)) {
+            throw new Error('notify: ' + JSON.stringify(notify.join(',')));
+        }
+        notify = notify.join(',');
+    }
+    var orcpt = params.orcpt ? params.orcpt.toString() : null;
+    return {
+        ret: ret,
+        envid: envid,
+        notify: notify,
+        orcpt: orcpt
+    };
+};
+
+SMTPConnection.prototype._getDsnRcptToArgs = function () {
+    var args = [];
+    // If the server supports DSN and the envelope includes an DSN prop
+    // then append DSN params to the RCPT TO command
+    if (this._envelope.dsn && this._supportedExtensions.indexOf('DSN') >= 0) {
+        if (this._envelope.dsn.notify) {
+            args.push('NOTIFY=' + this._envelope.dsn.notify);
+        }
+        if (this._envelope.dsn.orcpt) {
+            args.push('ORCPT=' + this._envelope.dsn.orcpt);
+        }
+    }
+    return (args.length ? ' ' + args.join(' ') : '');
+};
+
+SMTPConnection.prototype._createSendStream = function (callback) {
+    var dataStream = new DataStream();
+    var logStream;
+
+    if (this.options.lmtp) {
+        this._envelope.accepted.forEach(function (recipient, i) {
+            var final = i === this._envelope.accepted.length - 1;
+            this._responseActions.push(function (str) {
+                this._actionLMTPStream(recipient, final, str, callback);
+            }.bind(this));
+        }.bind(this));
+    } else {
+        this._responseActions.push(function (str) {
+            this._actionSMTPStream(str, callback);
+        }.bind(this));
+    }
+
+    dataStream.pipe(this._socket, {
+        end: false
+    });
+
+    if (this.options.debug) {
+        logStream = new PassThrough();
+        logStream.on('readable', function () {
+            var chunk;
+            while ((chunk = logStream.read())) {
+                this.logger.debug('[%s] C: %s', this.id, chunk.toString('binary').replace(/\r?\n$/, ''));
+            }
+        }.bind(this));
+        dataStream.pipe(logStream);
+    }
+
+    dataStream.once('end', function () {
+        this.logger.info('[%s] C: <%s bytes encoded mime message (source size %s bytes)>', this.id, dataStream.outByteCount, dataStream.inByteCount);
+    }.bind(this));
+
+    return dataStream;
+};
+
+/** ACTIONS **/
+
+/**
+ * Will be run after the connection is created and the server sends
+ * a greeting. If the incoming message starts with 220 initiate
+ * SMTP session by sending EHLO command
+ *
+ * @param {String} str Message from the server
+ */
+SMTPConnection.prototype._actionGreeting = function (str) {
+    clearTimeout(this._greetingTimeout);
+
+    if (str.substr(0, 3) !== '220') {
+        this._onError(new Error('Invalid greeting from server:\n' + str), 'EPROTOCOL', str, 'CONN');
+        return;
+    }
+
+    if (this.options.lmtp) {
+        this._responseActions.push(this._actionLHLO);
+        this._sendCommand('LHLO ' + this.name);
+    } else {
+        this._responseActions.push(this._actionEHLO);
+        this._sendCommand('EHLO ' + this.name);
+    }
+};
+
+/**
+ * Handles server response for LHLO command. If it yielded in
+ * error, emit 'error', otherwise treat this as an EHLO response
+ *
+ * @param {String} str Message from the server
+ */
+SMTPConnection.prototype._actionLHLO = function (str) {
+    if (str.charAt(0) !== '2') {
+        this._onError(new Error('Invalid response for LHLO:\n' + str), 'EPROTOCOL', str, 'LHLO');
+        return;
+    }
+
+    this._actionEHLO(str);
+};
+
+/**
+ * Handles server response for EHLO command. If it yielded in
+ * error, try HELO instead, otherwise initiate TLS negotiation
+ * if STARTTLS is supported by the server or move into the
+ * authentication phase.
+ *
+ * @param {String} str Message from the server
+ */
+SMTPConnection.prototype._actionEHLO = function (str) {
+    var match;
+
+    if (str.substr(0, 3) === '421') {
+        this._onError(new Error('Server terminates connection:\n' + str), 'ECONNECTION', str, 'EHLO');
+        return;
+    }
+
+    if (str.charAt(0) !== '2') {
+        if (this.options.requireTLS) {
+            this._onError(new Error('EHLO failed but HELO does not support required STARTTLS:\n' + str), 'ECONNECTION', str, 'EHLO');
+            return;
+        }
+
+        // Try HELO instead
+        this._responseActions.push(this._actionHELO);
+        this._sendCommand('HELO ' + this.name);
+        return;
+    }
+
+    // Detect if the server supports STARTTLS
+    if (!this.secure && !this.options.ignoreTLS && (/[ \-]STARTTLS\b/mi.test(str) || this.options.requireTLS)) {
+        this._sendCommand('STARTTLS');
+        this._responseActions.push(this._actionSTARTTLS);
+        return;
+    }
+
+    // Detect if the server supports SMTPUTF8
+    if (/[ \-]SMTPUTF8\b/mi.test(str)) {
+        this._supportedExtensions.push('SMTPUTF8');
+    }
+
+    // Detect if the server supports DSN
+    if (/[ \-]DSN\b/mi.test(str)) {
+        this._supportedExtensions.push('DSN');
+    }
+
+    // Detect if the server supports 8BITMIME
+    if (/[ \-]8BITMIME\b/mi.test(str)) {
+        this._supportedExtensions.push('8BITMIME');
+    }
+
+    // Detect if the server supports PIPELINING
+    if (/[ \-]PIPELINING\b/mi.test(str)) {
+        this._supportedExtensions.push('PIPELINING');
+    }
+
+    // Detect if the server supports PLAIN auth
+    if (/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)PLAIN/i.test(str)) {
+        this._supportedAuth.push('PLAIN');
+    }
+
+    // Detect if the server supports LOGIN auth
+    if (/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)LOGIN/i.test(str)) {
+        this._supportedAuth.push('LOGIN');
+    }
+
+    // Detect if the server supports CRAM-MD5 auth
+    if (/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)CRAM-MD5/i.test(str)) {
+        this._supportedAuth.push('CRAM-MD5');
+    }
+
+    // Detect if the server supports XOAUTH2 auth
+    if (/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)XOAUTH2/i.test(str)) {
+        this._supportedAuth.push('XOAUTH2');
+    }
+
+    // Detect if the server supports SIZE extensions (and the max allowed size)
+    if ((match = str.match(/[ \-]SIZE(?:\s+(\d+))?/mi))) {
+        this._supportedExtensions.push('SIZE');
+        this._maxAllowedSize = Number(match[1]) || 0;
+    }
+
+    this.emit('connect');
+};
+
+/**
+ * Handles server response for HELO command. If it yielded in
+ * error, emit 'error', otherwise move into the authentication phase.
+ *
+ * @param {String} str Message from the server
+ */
+SMTPConnection.prototype._actionHELO = function (str) {
+    if (str.charAt(0) !== '2') {
+        this._onError(new Error('Invalid response for EHLO/HELO:\n' + str), 'EPROTOCOL', str, 'HELO');
+        return;
+    }
+
+    this.emit('connect');
+};
+
+/**
+ * Handles server response for STARTTLS command. If there's an error
+ * try HELO instead, otherwise initiate TLS upgrade. If the upgrade
+ * succeedes restart the EHLO
+ *
+ * @param {String} str Message from the server
+ */
+SMTPConnection.prototype._actionSTARTTLS = function (str) {
+    if (str.charAt(0) !== '2') {
+        if (this.options.opportunisticTLS) {
+            this.logger.info('[%s] Failed STARTTLS upgrade, continuing unencrypted', this.id);
+            return this.emit('connect');
+        }
+        this._onError(new Error('Error upgrading connection with STARTTLS'), 'ETLS', str, 'STARTTLS');
+        return;
+    }
+
+    this._upgradeConnection(function (err, secured) {
+        if (err) {
+            this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'STARTTLS');
+            return;
+        }
+
+        this.logger.info('[%s] Connection upgraded with STARTTLS', this.id);
+
+        if (secured) {
+            // restart session
+            this._responseActions.push(this._actionEHLO);
+            this._sendCommand('EHLO ' + this.name);
+        } else {
+            this.emit('connect');
+        }
+    }.bind(this));
+};
+
+/**
+ * Handle the response for AUTH LOGIN command. We are expecting
+ * '334 VXNlcm5hbWU6' (base64 for 'Username:'). Data to be sent as
+ * response needs to be base64 encoded username.
+ *
+ * @param {String} str Message from the server
+ */
+SMTPConnection.prototype._actionAUTH_LOGIN_USER = function (str, callback) {
+    if (str !== '334 VXNlcm5hbWU6') {
+        callback(this._formatError('Invalid login sequence while waiting for "334 VXNlcm5hbWU6"', 'EAUTH', str, 'AUTH LOGIN'));
+        return;
+    }
+
+    this._responseActions.push(function (str) {
+        this._actionAUTH_LOGIN_PASS(str, callback);
+    }.bind(this));
+
+    this._sendCommand(new Buffer(this._auth.user + '', 'utf-8').toString('base64'));
+};
+
+/**
+ * Handle the response for AUTH NTLM, which should be a
+ * '334 <challenge string>'. See http://davenport.sourceforge.net/ntlm.html
+ * We already sent the Type1 message, the challenge is a Type2 message, we
+ * need to respond with a Type3 message.
+ *
+ * @param {String} str Message from the server
+ */
+SMTPConnection.prototype._actionAUTH_NTLM_TYPE1 = function (str, callback) {
+    var challengeMatch = str.match(/^334\s+(.+)$/);
+    var challengeString = '';
+
+    if (!challengeMatch) {
+        return callback(this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH NTLM'));
+    } else {
+        challengeString = challengeMatch[1];
+    }
+
+    if (!/^NTLM/i.test(challengeString)) {
+        challengeString = 'NTLM ' + challengeString;
+    }
+
+    var type2Message = ntlm.parseType2Message(challengeString, callback);
+    if (!type2Message) {
+        return;
+    }
+
+    var type3Message = ntlm.createType3Message(type2Message, {
+        domain: this._auth.domain || '',
+        workstation: this._auth.workstation || '',
+        username: this._auth.user,
+        password: this._auth.pass
+    });
+
+    type3Message = type3Message.substring(5); // remove the "NTLM " prefix
+
+    this._responseActions.push(function (str) {
+        this._actionAUTH_NTLM_TYPE3(str, callback);
+    }.bind(this));
+
+    this._sendCommand(type3Message);
+};
+
+/**
+ * Handle the response for AUTH CRAM-MD5 command. We are expecting
+ * '334 <challenge string>'. Data to be sent as response needs to be
+ * base64 decoded challenge string, MD5 hashed using the password as
+ * a HMAC key, prefixed by the username and a space, and finally all
+ * base64 encoded again.
+ *
+ * @param {String} str Message from the server
+ */
+SMTPConnection.prototype._actionAUTH_CRAM_MD5 = function (str, callback) {
+    var challengeMatch = str.match(/^334\s+(.+)$/);
+    var challengeString = '';
+
+    if (!challengeMatch) {
+        return callback(this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH CRAM-MD5'));
+    } else {
+        challengeString = challengeMatch[1];
+    }
+
+    // Decode from base64
+    var base64decoded = new Buffer(challengeString, 'base64').toString('ascii'),
+        hmac_md5 = crypto.createHmac('md5', this._auth.pass);
+
+    hmac_md5.update(base64decoded);
+
+    var hex_hmac = hmac_md5.digest('hex'),
+        prepended = this._auth.user + ' ' + hex_hmac;
+
+    this._responseActions.push(function (str) {
+        this._actionAUTH_CRAM_MD5_PASS(str, callback);
+    }.bind(this));
+
+
+    this._sendCommand(new Buffer(prepended).toString('base64'));
+};
+
+/**
+ * Handles the response to CRAM-MD5 authentication, if there's no error,
+ * the user can be considered logged in. Start waiting for a message to send
+ *
+ * @param {String} str Message from the server
+ */
+SMTPConnection.prototype._actionAUTH_CRAM_MD5_PASS = function (str, callback) {
+    if (!str.match(/^235\s+/)) {
+        return callback(this._formatError('Invalid login sequence while waiting for "235"', 'EAUTH', str, 'AUTH CRAM-MD5'));
+    }
+
+    this.logger.info('[%s] User %s authenticated', this.id, JSON.stringify(this._user));
+    this.authenticated = true;
+    callback(null, true);
+};
+
+/**
+ * Handles the TYPE3 response for NTLM authentication, if there's no error,
+ * the user can be considered logged in. Start waiting for a message to send
+ *
+ * @param {String} str Message from the server
+ */
+SMTPConnection.prototype._actionAUTH_NTLM_TYPE3 = function (str, callback) {
+    if (!str.match(/^235\s+/)) {
+        return callback(this._formatError('Invalid login sequence while waiting for "235"', 'EAUTH', str, 'AUTH NTLM'));
+    }
+
+    this.logger.info('[%s] User %s authenticated', this.id, JSON.stringify(this._user));
+    this.authenticated = true;
+    callback(null, true);
+};
+
+/**
+ * Handle the response for AUTH LOGIN command. We are expecting
+ * '334 UGFzc3dvcmQ6' (base64 for 'Password:'). Data to be sent as
+ * response needs to be base64 encoded password.
+ *
+ * @param {String} str Message from the server
+ */
+SMTPConnection.prototype._actionAUTH_LOGIN_PASS = function (str, callback) {
+    if (str !== '334 UGFzc3dvcmQ6') {
+        return callback(this._formatError('Invalid login sequence while waiting for "334 UGFzc3dvcmQ6"', 'EAUTH', str, 'AUTH LOGIN'));
+    }
+
+    this._responseActions.push(function (str) {
+        this._actionAUTHComplete(str, callback);
+    }.bind(this));
+
+    this._sendCommand(new Buffer(this._auth.pass + '', 'utf-8').toString('base64'));
+};
+
+/**
+ * Handles the response for authentication, if there's no error,
+ * the user can be considered logged in. Start waiting for a message to send
+ *
+ * @param {String} str Message from the server
+ */
+SMTPConnection.prototype._actionAUTHComplete = function (str, isRetry, callback) {
+    if (!callback && typeof isRetry === 'function') {
+        callback = isRetry;
+        isRetry = undefined;
+    }
+
+    if (str.substr(0, 3) === '334') {
+        this._responseActions.push(function (str) {
+            if (isRetry || !this._auth.xoauth2 || typeof this._auth.xoauth2 !== 'object') {
+                this._actionAUTHComplete(str, true, callback);
+            } else {
+                setTimeout(this._handleXOauth2Token.bind(this, true, callback), Math.random() * 4000 + 1000);
+            }
+        }.bind(this));
+        this._sendCommand('');
+        return;
+    }
+
+    if (str.charAt(0) !== '2') {
+        this.logger.info('[%s] User %s failed to authenticate', this.id, JSON.stringify(this._user));
+        return callback(this._formatError('Invalid login', 'EAUTH', str, 'AUTH ' + this._authMethod));
+    }
+
+    this.logger.info('[%s] User %s authenticated', this.id, JSON.stringify(this._user));
+    this.authenticated = true;
+    callback(null, true);
+};
+
+/**
+ * Handle response for a MAIL FROM: command
+ *
+ * @param {String} str Message from the server
+ */
+SMTPConnection.prototype._actionMAIL = function (str, callback) {
+    var message, curRecipient;
+    if (Number(str.charAt(0)) !== 2) {
+        if (this._usingSmtpUtf8 && /^550 /.test(str) && /[\x80-\uFFFF]/.test(this._envelope.from)) {
+            message = 'Internationalized mailbox name not allowed';
+        } else {
+            message = 'Mail command failed';
+        }
+        return callback(this._formatError(message, 'EENVELOPE', str, 'MAIL FROM'));
+    }
+
+    if (!this._envelope.rcptQueue.length) {
+        return callback(this._formatError('Can\'t send mail - no recipients defined', 'EENVELOPE', false, 'API'));
+    } else {
+        this._recipientQueue = [];
+
+        if (this._supportedExtensions.indexOf('PIPELINING') >= 0) {
+            while (this._envelope.rcptQueue.length) {
+                curRecipient = this._envelope.rcptQueue.shift();
+                this._recipientQueue.push(curRecipient);
+                this._responseActions.push(function (str) {
+                    this._actionRCPT(str, callback);
+                }.bind(this));
+                this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
+            }
+        } else {
+            curRecipient = this._envelope.rcptQueue.shift();
+            this._recipientQueue.push(curRecipient);
+            this._responseActions.push(function (str) {
+                this._actionRCPT(str, callback);
+            }.bind(this));
+            this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
+        }
+    }
+};
+
+/**
+ * Handle response for a RCPT TO: command
+ *
+ * @param {String} str Message from the server
+ */
+SMTPConnection.prototype._actionRCPT = function (str, callback) {
+    var message, err, curRecipient = this._recipientQueue.shift();
+    if (Number(str.charAt(0)) !== 2) {
+        // this is a soft error
+        if (this._usingSmtpUtf8 && /^553 /.test(str) && /[\x80-\uFFFF]/.test(curRecipient)) {
+            message = 'Internationalized mailbox name not allowed';
+        } else {
+            message = 'Recipient command failed';
+        }
+        this._envelope.rejected.push(curRecipient);
+        // store error for the failed recipient
+        err = this._formatError(message, 'EENVELOPE', str, 'RCPT TO');
+        err.recipient = curRecipient;
+        this._envelope.rejectedErrors.push(err);
+    } else {
+        this._envelope.accepted.push(curRecipient);
+    }
+
+    if (!this._envelope.rcptQueue.length && !this._recipientQueue.length) {
+        if (this._envelope.rejected.length < this._envelope.to.length) {
+            this._responseActions.push(function (str) {
+                this._actionDATA(str, callback);
+            }.bind(this));
+            this._sendCommand('DATA');
+        } else {
+            err = this._formatError('Can\'t send mail - all recipients were rejected', 'EENVELOPE', str, 'RCPT TO');
+            err.rejected = this._envelope.rejected;
+            err.rejectedErrors = this._envelope.rejectedErrors;
+            return callback(err);
+        }
+    } else if (this._envelope.rcptQueue.length) {
+        curRecipient = this._envelope.rcptQueue.shift();
+        this._recipientQueue.push(curRecipient);
+        this._responseActions.push(function (str) {
+            this._actionRCPT(str, callback);
+        }.bind(this));
+        this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
+    }
+};
+
+/**
+ * Handle response for a DATA command
+ *
+ * @param {String} str Message from the server
+ */
+SMTPConnection.prototype._actionDATA = function (str, callback) {
+    // response should be 354 but according to this issue https://github.com/eleith/emailjs/issues/24
+    // some servers might use 250 instead, so lets check for 2 or 3 as the first digit
+    if ([2, 3].indexOf(Number(str.charAt(0))) < 0) {
+        return callback(this._formatError('Data command failed', 'EENVELOPE', str, 'DATA'));
+    }
+
+    var response = {
+        accepted: this._envelope.accepted,
+        rejected: this._envelope.rejected
+    };
+
+    if (this._envelope.rejectedErrors.length) {
+        response.rejectedErrors = this._envelope.rejectedErrors;
+    }
+
+    callback(null, response);
+};
+
+/**
+ * Handle response for a DATA stream when using SMTP
+ * We expect a single response that defines if the sending succeeded or failed
+ *
+ * @param {String} str Message from the server
+ */
+SMTPConnection.prototype._actionSMTPStream = function (str, callback) {
+    if (Number(str.charAt(0)) !== 2) {
+        // Message failed
+        return callback(this._formatError('Message failed', 'EMESSAGE', str, 'DATA'));
+    } else {
+        // Message sent succesfully
+        return callback(null, str);
+    }
+};
+
+/**
+ * Handle response for a DATA stream
+ * We expect a separate response for every recipient. All recipients can either
+ * succeed or fail separately
+ *
+ * @param {String} recipient The recipient this response applies to
+ * @param {Boolean} final Is this the final recipient?
+ * @param {String} str Message from the server
+ */
+SMTPConnection.prototype._actionLMTPStream = function (recipient, final, str, callback) {
+    var err;
+    if (Number(str.charAt(0)) !== 2) {
+        // Message failed
+        err = this._formatError('Message failed for recipient ' + recipient, 'EMESSAGE', str, 'DATA');
+        err.recipient = recipient;
+        this._envelope.rejected.push(recipient);
+        this._envelope.rejectedErrors.push(err);
+        for (var i = 0, len = this._envelope.accepted.length; i < len; i++) {
+            if (this._envelope.accepted[i] === recipient) {
+                this._envelope.accepted.splice(i, 1);
+            }
+        }
+    }
+    if (final) {
+        return callback(null, str);
+    }
+};
+
+SMTPConnection.prototype._handleXOauth2Token = function (isRetry, callback) {
+    this._responseActions.push(function (str) {
+        this._actionAUTHComplete(str, isRetry, callback);
+    }.bind(this));
+
+    if (this._auth.xoauth2 && typeof this._auth.xoauth2 === 'object') {
+        this._auth.xoauth2[isRetry ? 'generateToken' : 'getToken'](function (err, token) {
+            if (err) {
+                this.logger.info('[%s] User %s failed to authenticate', this.id, JSON.stringify(this._user));
+                return callback(this._formatError(err, 'EAUTH', false, 'AUTH XOAUTH2'));
+            }
+            this._sendCommand('AUTH XOAUTH2 ' + token);
+        }.bind(this));
+    } else {
+        this._sendCommand('AUTH XOAUTH2 ' + this._buildXOAuth2Token(this._auth.user, this._auth.xoauth2));
+    }
+};
+
+/**
+ * Builds a login token for XOAUTH2 authentication command
+ *
+ * @param {String} user E-mail address of the user
+ * @param {String} token Valid access token for the user
+ * @return {String} Base64 formatted login token
+ */
+SMTPConnection.prototype._buildXOAuth2Token = function (user, token) {
+    var authData = [
+        'user=' + (user || ''),
+        'auth=Bearer ' + token,
+        '',
+        ''
+    ];
+    return new Buffer(authData.join('\x01')).toString('base64');
+};
+
+SMTPConnection.prototype._getHostname = function () {
+    // defaul hostname is machine hostname or [IP]
+    var defaultHostname = os.hostname() || '';
+
+    // ignore if not FQDN
+    if (defaultHostname.indexOf('.') < 0) {
+        defaultHostname = '[127.0.0.1]';
+    }
+
+    // IP should be enclosed in []
+    if (defaultHostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
+        defaultHostname = '[' + defaultHostname + ']';
+    }
+
+    return defaultHostname;
+};
diff --git a/node_modules/smtp-connection/package.json b/node_modules/smtp-connection/package.json
new file mode 100644
index 0000000..ef27bfb
--- /dev/null
+++ b/node_modules/smtp-connection/package.json
@@ -0,0 +1,103 @@
+{
+  "_args": [
+    [
+      {
+        "raw": "smtp-connection@2.12.0",
+        "scope": null,
+        "escapedName": "smtp-connection",
+        "name": "smtp-connection",
+        "rawSpec": "2.12.0",
+        "spec": "2.12.0",
+        "type": "version"
+      },
+      "/Users/fzy/project/koa2_Sequelize_project/node_modules/nodemailer-smtp-transport"
+    ]
+  ],
+  "_from": "smtp-connection@2.12.0",
+  "_id": "smtp-connection@2.12.0",
+  "_inCache": true,
+  "_location": "/smtp-connection",
+  "_nodeVersion": "6.5.0",
+  "_npmOperationalInternal": {
+    "host": "packages-12-west.internal.npmjs.com",
+    "tmp": "tmp/smtp-connection-2.12.0.tgz_1473079427239_0.7930881839711219"
+  },
+  "_npmUser": {
+    "name": "andris",
+    "email": "andris@kreata.ee"
+  },
+  "_npmVersion": "3.10.3",
+  "_phantomChildren": {},
+  "_requested": {
+    "raw": "smtp-connection@2.12.0",
+    "scope": null,
+    "escapedName": "smtp-connection",
+    "name": "smtp-connection",
+    "rawSpec": "2.12.0",
+    "spec": "2.12.0",
+    "type": "version"
+  },
+  "_requiredBy": [
+    "/nodemailer-smtp-transport"
+  ],
+  "_resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.12.0.tgz",
+  "_shasum": "d76ef9127cb23c2259edb1e8349c2e8d5e2d74c1",
+  "_shrinkwrap": null,
+  "_spec": "smtp-connection@2.12.0",
+  "_where": "/Users/fzy/project/koa2_Sequelize_project/node_modules/nodemailer-smtp-transport",
+  "author": {
+    "name": "Andris Reinman"
+  },
+  "bugs": {
+    "url": "https://github.com/andris9/smtp-connection/issues"
+  },
+  "dependencies": {
+    "httpntlm": "1.6.1",
+    "nodemailer-shared": "1.1.0"
+  },
+  "description": "Connect to SMTP servers",
+  "devDependencies": {
+    "chai": "^3.5.0",
+    "grunt": "^1.0.1",
+    "grunt-cli": "^1.2.0",
+    "grunt-eslint": "^19.0.0",
+    "grunt-mocha-test": "^0.12.7",
+    "mocha": "^3.0.2",
+    "proxy-test-server": "^1.0.0",
+    "sinon": "^1.17.5",
+    "smtp-server": "^1.14.2",
+    "xoauth2": "^1.2.0"
+  },
+  "directories": {
+    "test": "test"
+  },
+  "dist": {
+    "shasum": "d76ef9127cb23c2259edb1e8349c2e8d5e2d74c1",
+    "tarball": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.12.0.tgz"
+  },
+  "gitHead": "16fcb9f4b9ebc6419f61c0c45d790df7ee4531fe",
+  "homepage": "https://github.com/andris9/smtp-connection",
+  "keywords": [
+    "SMTP"
+  ],
+  "license": "MIT",
+  "main": "lib/smtp-connection.js",
+  "maintainers": [
+    {
+      "name": "andris",
+      "email": "andris@node.ee"
+    }
+  ],
+  "name": "smtp-connection",
+  "optionalDependencies": {},
+  "readme": "# smtp-connection\n\nSMTP client module. Connect to SMTP servers and send mail with it.\n\nThis module is the successor for the client part of the (now deprecated) SMTP module [simplesmtp](https://www.npmjs.com/package/simplesmtp). For matching SMTP server see [smtp-server](https://www.npmjs.com/package/smtp-server).\n\n[![Build Status](https://secure.travis-ci.org/nodemailer/smtp-connection.svg)](http://travis-ci.org/nodemailer/smtp-connection) [![npm version](https://badge.fury.io/js/smtp-connection.svg)](http://badge.fury.io/js/smtp-connection)\n\n## Usage\n\nInstall with npm\n\n```\nnpm install smtp-connection\n```\n\nRequire in your script\n\n```\nvar SMTPConnection = require('smtp-connection');\n```\n\n### Create SMTPConnection instance\n\n```javascript\nvar connection = new SMTPConnection(options);\n```\n\nWhere\n\n- **options** defines connection data\n\n  - **options.port** is the port to connect to (defaults to 25 or 465)\n  - **options.host** is the hostname or IP address to connect to (defaults to 'localhost')\n  - **options.secure** defines if the connection should use SSL (if `true`) or not (if `false`)\n  - **options.ignoreTLS** turns off STARTTLS support if true\n  - **options.requireTLS** forces the client to use STARTTLS. Returns an error if upgrading the connection is not possible or fails.\n  - **options.opportunisticTLS** tries to use STARTTLS and continues normally if it fails\n  - **options.name** optional hostname of the client, used for identifying to the server\n  - **options.localAddress** is the local interface to bind to for network connections\n  - **options.connectionTimeout** how many milliseconds to wait for the connection to establish\n  - **options.greetingTimeout** how many milliseconds to wait for the greeting after connection is established\n  - **options.socketTimeout** how many milliseconds of inactivity to allow\n  - **options.logger** optional [bunyan](https://github.com/trentm/node-bunyan) compatible logger instance. If set to `true` then logs to console. If value is not set or is `false` then nothing is logged\n  - **options.debug** if set to true, then logs SMTP traffic, otherwise logs only transaction events\n  - **options.authMethod** defines preferred authentication method, e.g. 'PLAIN'\n  - **options.tls** defines additional options to be passed to the socket constructor, e.g. _{rejectUnauthorized: true}_\n  - **options.socket** - initialized socket to use instead of creating a new one\n  - **options.connection** - connected socket to use instead of creating and connecting a new one. If `secure` option is true, then socket is upgraded from plaintext to ciphertext\n\n### Events\n\nSMTPConnection instances are event emitters with the following events\n\n- **'error'** _(err)_ emitted when an error occurs. Connection is closed automatically in this case.\n- **'connect'** emitted when the connection is established\n- **'end'** when the instance is destroyed\n\n### connect\n\nEstablish the connection\n\n```javascript\nconnection.connect(callback)\n```\n\nWhere\n\n- **callback** is the function to run once the connection is established. The function is added as a listener to the 'connect' event.\n\nAfter the connect event the `connection` has the following properties:\n\n- **connection.secure** - if `true` then the connection uses a TLS socket, otherwise it is using a cleartext socket. Connection can start out as cleartext but if available (or `requireTLS` is set to true) connection upgrade is tried\n\n### login\n\nIf the server requires authentication you can login with\n\n```javascript\nconnection.login(auth, callback)\n```\n\nWhere\n\n- **auth** is the authentication object\n\n  - **auth.user** is the username\n  - **auth.pass** is the password for the user\n  - **auth.xoauth2** is the OAuth2 access token (preferred if both `pass` and `xoauth2` values are set) or an [XOAuth2](https://github.com/andris9/xoauth2) token generator object.\n\n- **callback** is the callback to run once the authentication is finished. Callback has the following arguments\n\n  - **err** and error object if authentication failed\n\nIf a [XOAuth2](https://github.com/andris9/xoauth2) token generator is used as the value for `auth.xoauth2` then you do not need to set `auth.user`. XOAuth2 generator generates required accessToken itself if it is missing or expired. In this case if the authentication fails, a new token is requeested and the authentication is retried. If it still fails, an error is returned.\n\n**XOAuth2 Example**\n\n```javascript\nvar generator = require('xoauth2').createXOAuth2Generator({\n    user: '{username}',\n    clientId: '{Client ID}',\n    clientSecret: '{Client Secret}',\n    refreshToken: '{refresh-token}'\n});\n\n// listen for token updates\n// you probably want to store these to a db\ngenerator.on('token', function(token){\n    console.log('New token for %s: %s', token.user, token.accessToken);\n});\n\n// login\nconnection.login({\n    xoauth2: generator\n}, callback);\n```\n\n### Login using NTLM\n\n`smtp-connection` has experimental support for NTLM authentication. You can try it out like this:\n\n```javascript\nconnection.login({\n    domain: 'windows-domain',\n    workstation: 'windows-workstation',\n    user: 'user@somedomain.com',\n    pass: 'pass'\n}, callback);\n```\n\nI do not have access to an actual server that supports NTLM authentication so this feature is untested and should be used carefully.\n\n### send\n\nOnce the connection is authenticated (or just after connection is established if authentication is not required), you can send mail with\n\n```javascript\nconnection.send(envelope, message, callback)\n```\n\nWhere\n\n- **envelope** is the envelope object to use\n\n  - **envelope.from** is the sender address\n  - **envelope.to** is the recipient address or an array of addresses\n  - **envelope.size** is an optional value of the predicted size of the message in bytes. This value is used if the server supports the SIZE extension (RFC1870)\n  - **envelope.use8BitMime** if `true` then inform the server that this message might contain bytes outside 7bit ascii range\n  - **envelope.dsn** is the dsn options\n\n    - **envelope.dsn.ret** return either the full message 'FULL' or only headers 'HDRS'\n    - **envelope.dsn.envid** sender's 'envelope identifier' for tracking\n    - **envelope.dsn.notify** when to send a DSN. Multiple options are OK - array or comma delimited. NEVER must appear by itself. Available options: 'NEVER', 'SUCCESS', 'FAILURE', 'DELAY'\n    - **envelope.dsn.orcpt** original recipient\n\n- **message** is either a String, Buffer or a Stream. All newlines are converted to \\r\\n and all dots are escaped automatically, no need to convert anything before.\n\n- **callback** is the callback to run once the sending is finished or failed. Callback has the following arguments\n\n  - **err** and error object if sending failed\n\n    - **code** string code identifying the error, for example 'EAUTH' is returned when authentication fails\n    - **response** is the last response received from the server (if the error is caused by an error response from the server)\n    - **responseCode** is the numeric response code of the `response` string (if available)\n\n  - **info** information object about accepted and rejected recipients\n\n    - **accepted** an array of accepted recipient addresses. Normally this array should contain at least one address except when in LMTP mode. In this case the message itself might have succeeded but all recipients were rejected after sending the message.\n    - **rejected** an array of rejected recipient addresses. This array includes both the addresses that were rejected before sending the message and addresses rejected after sending it if using LMTP\n    - **rejectedErrors** if some recipients were rejected then this property holds an array of error objects for the rejected recipients\n    - **response** is the last response received from the server\n\n### quit\n\nUse it for graceful disconnect\n\n```javascript\nconnection.quit();\n```\n\n### close\n\nUse it for less graceful disconnect\n\n```javascript\nconnection.close();\n```\n\n### reset\n\nUse it to reset current session (invokes RSET command)\n\n```javascript\nconnection.reset(callback);\n```\n\n## License\n\n**MIT**\n",
+  "readmeFilename": "README.md",
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/andris9/smtp-connection.git"
+  },
+  "scripts": {
+    "test": "grunt mochaTest"
+  },
+  "version": "2.12.0"
+}
diff --git a/package.json b/package.json
index bbc9bc2..87d2937 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,8 @@
     "lodash": "^4.17.4",
     "mysql": "^2.14.1",
     "mysql2": "^1.4.1",
+    "nodemailer": "^4.0.1",
+    "nodemailer-smtp-transport": "^2.7.4",
     "pug": "^2.0.0-rc.1",
     "sequelize": "^4.5.0"
   },