From dc50d8c5bffdeb9c8e026adcdc7aa09cd7afab95 Mon Sep 17 00:00:00 2001 From: Alexandre Tiertant Date: Fri, 27 Apr 2018 13:57:47 +0200 Subject: [PATCH] add coveralls, eslint and update dependencies --- .eslintignore | 1 + .eslintrc | 26 +- .gitignore | 2 + .npmignore | 6 + connection.js | 92 ++-- messagestore.js | 142 +++--- package.json | 16 +- sasl.js | 98 ++-- server.js | 935 +++++++++++++++++++-------------------- states.js | 6 +- {tests => test}/index.js | 7 +- 11 files changed, 681 insertions(+), 650 deletions(-) create mode 100644 .eslintignore create mode 100644 .npmignore rename {tests => test}/index.js (98%) diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..b59f7e3 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +test/ \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index ef87b6d..92e44dd 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,11 +1,27 @@ { - "root": true, - "rules": { - "no-unused-vars": [0], - "no-undef": [0] + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" }, + "extends": "eslint:recommended", "env": { - "es6": true, "node": true + }, + "rules": { + "indent": [1, 2, { "SwitchCase": 1 }], + "no-undef": 1, + "no-console": 1, + "keyword-spacing": 1, + "no-trailing-spaces": 1, + "no-unused-vars": 1, + "no-redeclare": 1, + "no-unused-labels": 1, + "no-unreachable": 1, + "semi": [1, "always"], + "no-extra-semi": 1, + "no-fallthrough": 1, + "no-constant-condition": 1, + "no-mixed-spaces-and-tabs": 1, + "no-case-declarations": 1 } } diff --git a/.gitignore b/.gitignore index f5d0c3d..a7c5bc3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules .DS_Store *.log +.nyc_output/ +package-lock.json diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..08aab1c --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +.git +node_modules +.gitignore +package-lock.json +test +.nyc_output/ diff --git a/connection.js b/connection.js index 3520646..44fbe6f 100644 --- a/connection.js +++ b/connection.js @@ -4,30 +4,30 @@ const TEN_MINUTES = 10 * 60 * 1000; const States = require('./states'); var POP3Connnection = module.exports = function(server, socket, connection_id) { - debug('connection ' + connection_id, socket.remoteAddress); - var self = this; - this.socket = socket; - this.server = server; - this.connection_id = connection_id; - this.state = States.AUTHENTICATION; - this.UID = this.connection_id + "." + (+new Date()); - this.response("+OK POP3 Server ready <" + self.UID + "@" + self.server_name + ">"); -} + debug('connection ' + connection_id, socket.remoteAddress); + var self = this; + this.socket = socket; + this.server = server; + this.connection_id = connection_id; + this.state = States.AUTHENTICATION; + this.UID = this.connection_id + "." + (+new Date()); + this.response("+OK POP3 Server ready <" + self.UID + "@" + self.server_name + ">"); +}; POP3Connnection.prototype.response = function response(message) { - let resBuffer; - if (message.toString) { - message = message.toString(); - } - if (typeof message == "string") { - debug('responding', this.user, message.substring(0, 60)); - resBuffer = new Buffer(message + "\r\n", "utf-8"); - } else { - resBuffer = Buffer.concat([message, new Buffer("\r\n", "utf-8")]); - } + let resBuffer; + if (message.toString) { + message = message.toString(); + } + if (typeof message == "string") { + debug('responding', this.user, message.substring(0, 60)); + resBuffer = new Buffer(message + "\r\n", "utf-8"); + } else { + resBuffer = Buffer.concat([message, new Buffer("\r\n", "utf-8")]); + } - this.socket.write(resBuffer); -} + this.socket.write(resBuffer); +}; /** * POP3Server#destroy() -> undefined @@ -36,33 +36,33 @@ POP3Connnection.prototype.response = function response(message) { * do this by itself) **/ POP3Connnection.prototype.destroy = function destroy() { - const remoteAddress = this.socket ? this.socket.remoteAddress : null; - debug('destroying connection', this.user, this.connection_id, remoteAddress); + const remoteAddress = this.socket ? this.socket.remoteAddress : null; + debug('destroying connection', this.user, this.connection_id, remoteAddress); - if (this.timer) { - clearTimeout(this.timer); - } - this.timer = null; + if (this.timer) { + clearTimeout(this.timer); + } + this.timer = null; - if (this.server.connected_users[this.user]) { - delete this.server.connected_users[this.user]; - } + if (this.server.connected_users[this.user]) { + delete this.server.connected_users[this.user]; + } - if (this.socket && this.socket.end) { - this.socket.end(); - } -} + if (this.socket && this.socket.end) { + this.socket.end(); + } +}; // kill client after inactivity -POP3Connnection.prototype.updateTimeout = function () { - if (this.timer) clearTimeout(this.timer); - this.timer = setTimeout((function () { - if (!this.socket) - return; - if (this.state == States.TRANSACTION) { - this.state = States.UPDATE; - } - debug("Connection closed for client inactivity", this.user); - this.destroy(); - }).bind(this), TEN_MINUTES); -} \ No newline at end of file +POP3Connnection.prototype.updateTimeout = function() { + if (this.timer) clearTimeout(this.timer); + this.timer = setTimeout((function() { + if (!this.socket) + return; + if (this.state == States.TRANSACTION) { + this.state = States.UPDATE; + } + debug("Connection closed for client inactivity", this.user); + this.destroy(); + }).bind(this), TEN_MINUTES); +}; \ No newline at end of file diff --git a/messagestore.js b/messagestore.js index 808dab6..cb8cb49 100644 --- a/messagestore.js +++ b/messagestore.js @@ -6,60 +6,60 @@ var debug = require('debug')('messagestore'); * register, read and removeDeleted should be overriden on pop3server store option. */ var MessageStore = module.exports = function(connection) { - debug('MessageStore created', connection.user); - this.user = connection.user; - this.messages = []; - this.deletedMessages = []; - this.connection = connection; + debug('MessageStore created', connection.user); + this.user = connection.user; + this.messages = []; + this.deletedMessages = []; + this.connection = connection; }; MessageStore.prototype.register = function noopRegister(cb) { - debug('register has not been overridden by your implementation of MessageStore'); - cb(); + debug('register has not been overridden by your implementation of MessageStore'); + cb(); }; MessageStore.prototype.read = function noopRead(uid, cb) { - debug('read has not been overridden by your implementation of MessageStore'); - cb(null, ''); + debug('read has not been overridden by your implementation of MessageStore'); + cb(null, ''); }; MessageStore.prototype.removeDeleted = function noopRemoveDeleted(deleted, cb) { - debug('removeDeleted has not been overridden by your implementation of MessageStore'); - cb(); + debug('removeDeleted has not been overridden by your implementation of MessageStore'); + cb(); }; -MessageStore.prototype.addMessage = function (uid, length) { - this.messages.push({uid: uid, length: length}); - debug('addMessage', length, uid, this.user); +MessageStore.prototype.addMessage = function(uid, length) { + this.messages.push({ uid: uid, length: length }); + debug('addMessage', length, uid, this.user); }; -MessageStore.prototype.stat = function (callback) { - var count = 0; - var size = 0; - this.messages.forEach(function(msg){ - if (!msg.deleted) { - count++; - size += msg.length; - } - - }); - debug('stat', this.user, size); - callback(null, count, size); +MessageStore.prototype.stat = function(callback) { + var count = 0; + var size = 0; + this.messages.forEach(function(msg) { + if (!msg.deleted) { + count++; + size += msg.length; + } + + }); + debug('stat', this.user, size); + callback(null, count, size); }; -MessageStore.prototype.list = function (_msg, callback) { - debug('list', this.user, _msg); - var msg = _msg - 1; - if (msg >= 0 && typeof msg === 'number') { - if (!this.messages[msg]) { - return callback(null, null); - } - return callback(_msg + ' ' + this.messages[msg].length); +MessageStore.prototype.list = function(_msg, callback) { + debug('list', this.user, _msg); + var msg = _msg - 1; + if (msg >= 0 && typeof msg === 'number') { + if (!this.messages[msg]) { + return callback(null, null); } - var result = this.messages.map( - (m, index) => (index + 1) + ' ' + m.length - ); - callback(null, result); + return callback(_msg + ' ' + this.messages[msg].length); + } + var result = this.messages.map( + (m, index) => (index + 1) + ' ' + m.length + ); + callback(null, result); }; /** * Unique ID List @@ -67,19 +67,19 @@ MessageStore.prototype.list = function (_msg, callback) { * @param {Function} callback [description] * @return {[type]} [description] */ -MessageStore.prototype.uidl = function (_msg, callback) { - debug('uidl', this.user, _msg); - var msg = _msg - 1; - if (msg >= 0 && typeof msg === 'number') { - if (!this.messages[msg]) { - return callback(null, null); - } - return callback(null, _msg + ' ' + this.messages[msg].uid); +MessageStore.prototype.uidl = function(_msg, callback) { + debug('uidl', this.user, _msg); + var msg = _msg - 1; + if (msg >= 0 && typeof msg === 'number') { + if (!this.messages[msg]) { + return callback(null, null); } - var result = this.messages.map( - (m, index) => (index + 1) + ' ' + m.uid - ); - callback(null, result); + return callback(null, _msg + ' ' + this.messages[msg].uid); + } + var result = this.messages.map( + (m, index) => (index + 1) + ' ' + m.uid + ); + callback(null, result); }; /** * Retrieve the message @@ -87,30 +87,30 @@ MessageStore.prototype.uidl = function (_msg, callback) { * @param {Function} callback (err, Message) */ MessageStore.prototype.retr = function retr(_msg, callback) { - var msg = _msg - 1; - debug('retr', this.user, _msg); - var invalidIndex = isNaN(msg) || !this.messages[msg]; - if (invalidIndex) { - return callback(null, false); - } - this.read(this.messages[msg].uid, callback); + var msg = _msg - 1; + debug('retr', this.user, _msg); + var invalidIndex = isNaN(msg) || !this.messages[msg]; + if (invalidIndex) { + return callback(null, false); + } + this.read(this.messages[msg].uid, callback); }; MessageStore.prototype.dele = function dele(_msg, callback) { - var msg = _msg - 1; - debug('dele', this.user, msg); - var invalidIndex = isNaN(msg) || !this.messages[msg]; - if (invalidIndex) { - return callback(null, false); - } - // not actually removed at this time - will be removed when connection closes - this.messages[msg].deleted = true; - return callback(null, true); + var msg = _msg - 1; + debug('dele', this.user, msg); + var invalidIndex = isNaN(msg) || !this.messages[msg]; + if (invalidIndex) { + return callback(null, false); + } + // not actually removed at this time - will be removed when connection closes + this.messages[msg].deleted = true; + return callback(null, true); }; -MessageStore.prototype.rset = function () { - debug('rset', this.user); - this.messages.forEach(function(msg, index){ - delete msg.deleted; - }); +MessageStore.prototype.rset = function() { + debug('rset', this.user); + this.messages.forEach(function(msg) { + delete msg.deleted; + }); }; diff --git a/package.json b/package.json index 79d10d1..88177fa 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "description": "POP3 server for node.js", "main": "server.js", "scripts": { - "test": "mocha -R spec tests/" + "test": "nyc mocha --timeout=10000 --exit", + "coveralls": "nyc report --reporter=text-lcov | coveralls", + "lint": "eslint ./" }, "repository": { "type": "git", @@ -15,12 +17,16 @@ "node": ">=4.0.0" }, "dependencies": { - "debug": "^2.2.0" + "debug": "^3.1.0" }, "devDependencies": { - "chai": "~3.5.0", + "chai": "~4.1.2", + "coveralls": "^3.0.0", + "eslint": "~3.11.1", + "istanbul": "0.4.5", "mailx": "~0.0.11", - "mocha": "~3.0.2", - "mailcomposer": "~0.1.29" + "mocha": "~5.1.1", + "nodemailer": "~4.6.4", + "nyc": "~11.7.1" } } diff --git a/sasl.js b/sasl.js index b7adedd..327eb80 100644 --- a/sasl.js +++ b/sasl.js @@ -4,10 +4,10 @@ var crypto = require("crypto"); /** * sasl.AUTHMethods -> Object - * + * * object containing information about the authentication functions * Struncture: {name: authentication_function(authObject)} - * + * * authObject has the following structure: * - wait (Boolean): initially false. If set to TRUE, then the next response from * the client will be forwarded directly back to the function @@ -21,8 +21,8 @@ var crypto = require("crypto"); * returns TRUE if successful or FALSE if not **/ exports.AUTHMethods = { - "PLAIN": PLAIN, - "CRAM-MD5": CRAM_MD5 + "PLAIN": PLAIN, + "CRAM-MD5": CRAM_MD5 }; // AUTH PLAIN @@ -31,7 +31,7 @@ exports.AUTHMethods = { // STEP 1 // CLIENT: AUTH PLAIN // SERVER: + -// STEP 2 +// STEP 2 // CLIENT: BASE64(usernamepassword) // SERVER: +OK logged in @@ -40,29 +40,29 @@ exports.AUTHMethods = { // CLIENT: AUTH PLAIN BASE64(usernamepassword) // SERVER: +OK logged in -function PLAIN(authObj){ +function PLAIN(authObj) { - // Step 1 - if(!authObj.params){ - authObj.wait = true; - return "+ "; - } + // Step 1 + if (!authObj.params) { + authObj.wait = true; + return "+ "; + } - // Step 2 - var login = new Buffer(authObj.params, 'base64'); - var parts = login.toString('ascii').split("\u0000"); + // Step 2 + var login = new Buffer(authObj.params, 'base64'); + var parts = login.toString('ascii').split("\u0000"); - if (parts.length!=3 || !parts[1]) { - return "-ERR Invalid authentication data"; - } - - if (parts[0] != parts[1]) { // try to log in in behalf of some other user - return "-ERR [AUTH] Not authorized to requested authorization identity"; - } + if (parts.length != 3 || !parts[1]) { + return "-ERR Invalid authentication data"; + } + + if (parts[0] != parts[1]) { // try to log in in behalf of some other user + return "-ERR [AUTH] Not authorized to requested authorization identity"; + } - authObj.user = parts[1]; - return authObj.check(parts[1], parts[2]); + authObj.user = parts[1]; + return authObj.check(parts[1], parts[2]); } // AUTH CRAM-MD5 @@ -74,31 +74,31 @@ function PLAIN(authObj){ // CLIENT: BASE64(user HMAC-MD5(secret, password)) // SERVER: +OK Logged in -function CRAM_MD5(authObj){ - - var salt = "<"+authObj.connection.UID+"@"+authObj.connection.server.server_name+">"; - - // Step 1 - if(!authObj.params){ - authObj.wait = true; - return "+ " + new Buffer(salt).toString("base64"); - } - - // Step 2 - var params = new Buffer(authObj.params, 'base64').toString('ascii').split(" "); - var user = params && params[0]; - var challenge = params && params[1]; - if (!user || !challenge) { - return "-ERR Invalid authentication"; +function CRAM_MD5(authObj) { + + var salt = "<" + authObj.connection.UID + "@" + authObj.connection.server.server_name + ">"; + + // Step 1 + if (!authObj.params) { + authObj.wait = true; + return "+ " + new Buffer(salt).toString("base64"); + } + + // Step 2 + var params = new Buffer(authObj.params, 'base64').toString('ascii').split(" "); + var user = params && params[0]; + var challenge = params && params[1]; + if (!user || !challenge) { + return "-ERR Invalid authentication"; + } + return authObj.check(user, function(pass) { + var hmac = crypto.createHmac("md5", pass), digest; + hmac.update(salt); + digest = hmac.digest("hex"); + if (digest == challenge) { + authObj.user = user; + return true; } - return authObj.check(user, function(pass){ - var hmac = crypto.createHmac("md5", pass), digest; - hmac.update(salt); - digest = hmac.digest("hex"); - if (digest == challenge) { - authObj.user = user; - return true; - } - return false; - }); + return false; + }); } \ No newline at end of file diff --git a/server.js b/server.js index ddfd7da..604605f 100644 --- a/server.js +++ b/server.js @@ -2,7 +2,7 @@ const States = require('./states'); const crypto = require('crypto'); const debug = require('debug')('pop3-server'); -const os = require('os') +const os = require('os'); const net = require('net'); const tls = require('tls'); var MessageStore = require("./messagestore"); @@ -11,353 +11,353 @@ var SASL = require("./sasl"); function md5(str) { - const hash = crypto.createHash('md5'); - hash.update(str); - return hash.digest("hex").toLowerCase(); + const hash = crypto.createHash('md5'); + hash.update(str); + return hash.digest("hex").toLowerCase(); } var POP3Server = module.exports = function(options) { - var self = this; - this.options = options = options || {}; - this.server_name = options.serverName || os.hostname() || 'localhost'; - this.COUNTER = 0; - this.connected_users = {}; - this.sockets = []; + var self = this; + this.options = options = options || {}; + this.server_name = options.serverName || os.hostname() || 'localhost'; + this.COUNTER = 0; + this.connected_users = {}; + this.sockets = []; - this.authMethods = SASL.AUTHMethods; + this.authMethods = SASL.AUTHMethods; - Object.keys(options.authMethods || {}).forEach(function(k){ - self.authMethods[k] = options.authMethods[k]; - }); + Object.keys(options.authMethods || {}).forEach(function(k) { + self.authMethods[k] = options.authMethods[k]; + }); + + this.authCallback = options.auth || function(checkFn) { checkFn(); }; - this.authCallback = options.auth || function(checkFn) { checkFn(); }; - - // once we have the user name, this will be instantiated to `this.store` - this.MsgStore = MessageStore; - if (options.store) { - if (options.store.register) { - this.MsgStore.prototype.register = options.store.register; - } - if (options.store.read) { - this.MsgStore.prototype.read = options.store.read; - } - if (options.store.removeDeleted) { - this.MsgStore.prototype.removeDeleted = options.store.removeDeleted; - } + // once we have the user name, this will be instantiated to `this.store` + this.MsgStore = MessageStore; + if (options.store) { + if (options.store.register) { + this.MsgStore.prototype.register = options.store.register; + } + if (options.store.read) { + this.MsgStore.prototype.read = options.store.read; } + if (options.store.removeDeleted) { + this.MsgStore.prototype.removeDeleted = options.store.removeDeleted; + } + } - this.connection_secured = false; - this.tlsOptions = + this.connection_secured = false; + this.tlsOptions = this.capabilities = { - // AUTHENTICATION - 1: ["UIDL", "USER", "RESP-CODES", "AUTH-RESP-CODE"], - // TRANSACTION - 2: ["UIDL", "EXPIRE NEVER", "LOGIN-DELAY 0", "IMPLEMENTATION node.js POP3 server"], - // UPDATE - 3: [] + // AUTHENTICATION + 1: ["UIDL", "USER", "RESP-CODES", "AUTH-RESP-CODE"], + // TRANSACTION + 2: ["UIDL", "EXPIRE NEVER", "LOGIN-DELAY 0", "IMPLEMENTATION node.js POP3 server"], + // UPDATE + 3: [] }; }; -POP3Server.prototype.listen = function(port, callback){ - var self = this; - this.sockets.push(net.createServer(function(socket){ - var connection_id = ++self.COUNTER; - var connection = new POP3Connnection(self, socket, connection_id); - socket.on('data', function (data) { - self.onData(connection, data); - }); - socket.on('end', function (data) { - self.onEnd(connection, data); - }); - socket.on('error', function (err) { - self.onError(connection, err); - }); - }).listen(port, callback)); +POP3Server.prototype.listen = function(port, callback) { + var self = this; + this.sockets.push(net.createServer(function(socket) { + var connection_id = ++self.COUNTER; + var connection = new POP3Connnection(self, socket, connection_id); + socket.on('data', function(data) { + self.onData(connection, data); + }); + socket.on('end', function(data) { + self.onEnd(connection, data); + }); + socket.on('error', function(err) { + self.onError(connection, err); + }); + }).listen(port, callback)); }; -POP3Server.prototype.listenSSL = function(port, callback){ - var self = this; - if (!this.options.tls) { - return callback(new Error('listenSSL require tls options.')); - } - this.sockets.push(tls.createServer(this.options.tls, function(socket){ - var connection_id = ++self.COUNTER; - var connection = new POP3Connnection(self, socket, connection_id); - connection.secure = true; - socket.on('data', function (data) { - self.onData(connection, data); - }); - socket.on('end', function (data) { - self.onEnd(connection, data); - }); - socket.on('error', function (err) { - self.onError(connection, err); - }); - }).listen(port, callback)); +POP3Server.prototype.listenSSL = function(port, callback) { + var self = this; + if (!this.options.tls) { + return callback(new Error('listenSSL require tls options.')); + } + this.sockets.push(tls.createServer(this.options.tls, function(socket) { + var connection_id = ++self.COUNTER; + var connection = new POP3Connnection(self, socket, connection_id); + connection.secure = true; + socket.on('data', function(data) { + self.onData(connection, data); + }); + socket.on('end', function(data) { + self.onEnd(connection, data); + }); + socket.on('error', function(err) { + self.onError(connection, err); + }); + }).listen(port, callback)); }; POP3Server.prototype.close = function(callback) { - var self = this; - var count = 0; - this.sockets.forEach(function(socket) { - socket.close(function() { - count++; - if (count === self.sockets.length) { - callback(); - } - }); + var self = this; + var count = 0; + this.sockets.forEach(function(socket) { + socket.close(function() { + count++; + if (count === self.sockets.length) { + callback(); + } }); + }); }; -POP3Server.prototype.afterLogin = function (connection, callback) { - var self = this; - - if (connection.user && this.connected_users[connection.user]) { - return callback(null, "-ERR [IN-USE] You already have a POP session running"); - } - - if (connection.user) { - connection.store = new this.MsgStore(connection); - connection.store.register(function(err) { - if (err) { - return callback(err); - } - self.connected_users[connection.user.trim().toLowerCase()] = true; - return callback(null, true); - }); - } else { - callback(new Error('no user')); - } +POP3Server.prototype.afterLogin = function(connection, callback) { + var self = this; + + if (connection.user && this.connected_users[connection.user]) { + return callback(null, "-ERR [IN-USE] You already have a POP session running"); + } + + if (connection.user) { + connection.store = new this.MsgStore(connection); + connection.store.register(function(err) { + if (err) { + return callback(err); + } + self.connected_users[connection.user.trim().toLowerCase()] = true; + return callback(null, true); + }); + } else { + callback(new Error('no user')); + } }; -POP3Server.prototype.onData = function (connection, data) { - const request = data.toString("ascii", 0, data.length); - this.onCommand(connection, request); +POP3Server.prototype.onData = function(connection, data) { + const request = data.toString("ascii", 0, data.length); + this.onCommand(connection, request); }; -POP3Server.prototype.onEnd = function (connection, data) { - if (connection.state === null) { - return; - } - connection.state = States.UPDATE; - connection.destroy(); +POP3Server.prototype.onEnd = function(connection) { + if (connection.state === null) { + return; + } + connection.state = States.UPDATE; + connection.destroy(); }; -POP3Server.prototype.onError = function (connection, err) { - debug('socket error', connection.user, err.message); - connection.destroy(); +POP3Server.prototype.onError = function(connection, err) { + debug('socket error', connection.user, err.message); + connection.destroy(); }; -POP3Server.prototype.onCommand = function (connection, request) { - const cmd = request.match(/^[A-Za-z]+/); - let params = cmd && request.substr(cmd[0].length + 1); +POP3Server.prototype.onCommand = function(connection, request) { + const cmd = request.match(/^[A-Za-z]+/); + let params = cmd && request.substr(cmd[0].length + 1); - debug('onCommand', cmd, 'authState=', connection.authState); + debug('onCommand', cmd, 'authState=', connection.authState); - connection.updateTimeout(); + connection.updateTimeout(); - if (connection.authState) { - params = request.trim(); - return this.cmdAUTHNext(connection, params); - } + if (connection.authState) { + params = request.trim(); + return this.cmdAUTHNext(connection, params); + } - if (!cmd) { - return connection.response("-ERR"); - } + if (!cmd) { + return connection.response("-ERR"); + } - if (typeof this["cmd" + cmd[0].toUpperCase()] == "function") { - return this["cmd" + cmd[0].toUpperCase()](connection, params && params.trim()); - } + if (typeof this["cmd" + cmd[0].toUpperCase()] == "function") { + return this["cmd" + cmd[0].toUpperCase()](connection, params && params.trim()); + } - connection.response("-ERR"); -} + connection.response("-ERR"); +}; // Universal commands // CAPA - Reveals server capabilities to the client -POP3Server.prototype.cmdCAPA = function (connection, params) { - - if (params && params.length) { - return connection.response("-ERR Try: CAPA"); - } - - params = (params || "").split(" "); - connection.response("+OK Capability list follows"); - for (var i = 0; i < this.capabilities[connection.state].length; i++) { - connection.response(this.capabilities[connection.state][i]); - } - if (this.authMethods) { - var methods = []; - for (var i in this.authMethods) { - if (this.authMethods.hasOwnProperty(i)) { - methods.push(i); - } - } - if (methods.length && connection.state == States.AUTHENTICATION) { - connection.response("SASL " + methods.join(" ")); - } - } - if (this.options.tls && !connection.secure && connection.state === States.AUTHENTICATION) { - connection.response("STLS"); - } - connection.response("."); -} +POP3Server.prototype.cmdCAPA = function(connection, params) { + + if (params && params.length) { + return connection.response("-ERR Try: CAPA"); + } + + params = (params || "").split(" "); + connection.response("+OK Capability list follows"); + for (var i = 0; i < this.capabilities[connection.state].length; i++) { + connection.response(this.capabilities[connection.state][i]); + } + if (this.authMethods) { + var methods = []; + for (var j in this.authMethods) { + if (this.authMethods.hasOwnProperty(j)) { + methods.push(j); + } + } + if (methods.length && connection.state == States.AUTHENTICATION) { + connection.response("SASL " + methods.join(" ")); + } + } + if (this.options.tls && !connection.secure && connection.state === States.AUTHENTICATION) { + connection.response("STLS"); + } + connection.response("."); +}; // QUIT - Closes the connection -POP3Server.prototype.cmdQUIT = function (connection) { - var end = function() { - connection.response("+OK POP3 Server signing off"); - connection.socket.end(); - }; - if (connection.state == States.TRANSACTION) { - connection.state = States.UPDATE; - var deleted = []; - connection.store.messages.forEach(function(msg) { - if (msg.deleted) { - deleted.push(msg.uid); - } - }); - connection.store.removeDeleted(deleted, end); - } else { - end(); - } -} - -POP3Server.prototype.cmdSTLS = function (connection) { - var self = this; - if (connection.secure) { - return connection.response("-ERR Command not permitted when TLS active"); - } - if (connection.state != States.AUTHENTICATION) { - return connection.response("-ERR Unknown command: STLS"); - } - if (!this.options.tls) { - return connection.response("-ERR Unknown command: STLS"); - } - connection.response("+OK Begin TLS negotiation now."); - connection.socket.removeAllListeners(); - var socketOptions = { - secureContext: tls.createSecureContext(self.options.tls), - isServer: true - }; - connection.socket = new tls.TLSSocket(connection.socket, socketOptions); +POP3Server.prototype.cmdQUIT = function(connection) { + var end = function() { + connection.response("+OK POP3 Server signing off"); + connection.socket.end(); + }; + if (connection.state == States.TRANSACTION) { + connection.state = States.UPDATE; + var deleted = []; + connection.store.messages.forEach(function(msg) { + if (msg.deleted) { + deleted.push(msg.uid); + } + }); + connection.store.removeDeleted(deleted, end); + } else { + end(); + } +}; - connection.socket.on('secure', function () { - connection.secure = true; - connection.socket.on('data', function (data) { - self.onData(connection, data); - }); - connection.socket.on('end', function (data) { - self.onEnd(connection, data); - }); - connection.socket.on('error', function (err) { - self.onError(connection, err); - }); +POP3Server.prototype.cmdSTLS = function(connection) { + var self = this; + if (connection.secure) { + return connection.response("-ERR Command not permitted when TLS active"); + } + if (connection.state != States.AUTHENTICATION) { + return connection.response("-ERR Unknown command: STLS"); + } + if (!this.options.tls) { + return connection.response("-ERR Unknown command: STLS"); + } + connection.response("+OK Begin TLS negotiation now."); + connection.socket.removeAllListeners(); + var socketOptions = { + secureContext: tls.createSecureContext(self.options.tls), + isServer: true + }; + connection.socket = new tls.TLSSocket(connection.socket, socketOptions); + + connection.socket.on('secure', function() { + connection.secure = true; + connection.socket.on('data', function(data) { + self.onData(connection, data); }); -} + connection.socket.on('end', function(data) { + self.onEnd(connection, data); + }); + connection.socket.on('error', function(err) { + self.onError(connection, err); + }); + }); +}; // AUTHENTICATION commands // AUTH auth_engine - initiates an authentication request -POP3Server.prototype.cmdAUTH = function (connection, auth) { - if (connection.state != States.AUTHENTICATION) { - return connection.response("-ERR Only allowed in authentication mode"); - } - - if (!auth) { - return connection.response("-ERR Invalid authentication method"); - } - - var parts = auth.split(" "); - var method = parts.shift().toUpperCase().trim(); - var params = parts.join(" "); - var response; - - connection.authObj = { - wait: false, - params: params, - history: [], - connection: connection, - check: this.cmdAUTHCheck.bind(this) - }; - - // check if the asked auth methid exists and if so, then run it for the first time - if (typeof this.authMethods[method] == "function") { - response = this.authMethods[method](connection.authObj); - if (response) { - if (connection.authObj.wait) { - connection.authState = method; - connection.authObj.history.push(params); - } else if (response === true) { - return this.cmdDoAUTH(connection); - } - connection.response(response); - } else { - connection.authObj = false; - connection.response("-ERR [AUTH] Invalid authentication"); - } +POP3Server.prototype.cmdAUTH = function(connection, auth) { + if (connection.state != States.AUTHENTICATION) { + return connection.response("-ERR Only allowed in authentication mode"); + } + + if (!auth) { + return connection.response("-ERR Invalid authentication method"); + } + + var parts = auth.split(" "); + var method = parts.shift().toUpperCase().trim(); + var params = parts.join(" "); + var response; + + connection.authObj = { + wait: false, + params: params, + history: [], + connection: connection, + check: this.cmdAUTHCheck.bind(this) + }; + + // check if the asked auth methid exists and if so, then run it for the first time + if (typeof this.authMethods[method] == "function") { + response = this.authMethods[method](connection.authObj); + if (response) { + if (connection.authObj.wait) { + connection.authState = method; + connection.authObj.history.push(params); + } else if (response === true) { + return this.cmdDoAUTH(connection); + } + connection.response(response); } else { - connection.authObj = false; - connection.response("-ERR Unrecognized authentication type"); + connection.authObj = false; + connection.response("-ERR [AUTH] Invalid authentication"); } -} + } else { + connection.authObj = false; + connection.response("-ERR Unrecognized authentication type"); + } +}; /** * [cmdDoAUTH description] * @return {string} response */ -POP3Server.prototype.cmdDoAUTH = function (connection) { - connection.user = connection.authObj.user.trim().toLowerCase(); +POP3Server.prototype.cmdDoAUTH = function(connection) { + connection.user = connection.authObj.user.trim().toLowerCase(); + connection.authState = false; + connection.authObj = false; + this.afterLogin(connection, function(err, isAfterLogin) { + if (err) { + return connection.response('-ERR [SYS] Error with initializing'); + } + if (isAfterLogin === true) { + connection.state = States.TRANSACTION; + return connection.response('+OK You are now logged in'); + } + connection.response(isAfterLogin); + }); +}; + +POP3Server.prototype.cmdAUTHNext = function(connection, params) { + if (connection.state != States.AUTHENTICATION) { + return connection.response("-ERR Only allowed in authentication mode"); + } + connection.authObj.wait = false; + connection.authObj.params = params; + debug('cmdAUTHNext', connection.authState, this.authObj); + var response = this.authMethods[connection.authState](connection.authObj); + if (!response) { connection.authState = false; connection.authObj = false; - this.afterLogin(connection, function(err, isAfterLogin){ - if (err) { - return connection.response('-ERR [SYS] Error with initializing'); - } - if (isAfterLogin === true) { - connection.state = States.TRANSACTION; - return connection.response('+OK You are now logged in'); - } - connection.response(isAfterLogin); - }); -} - -POP3Server.prototype.cmdAUTHNext = function (connection, params) { - if (connection.state != States.AUTHENTICATION) { - return connection.response("-ERR Only allowed in authentication mode"); - } - connection.authObj.wait = false; - connection.authObj.params = params; - debug('cmdAUTHNext', connection.authState, this.authObj); - var response = this.authMethods[connection.authState](connection.authObj); - if (!response) { - connection.authState = false; - connection.authObj = false; - return connection.response("-ERR [AUTH] Invalid authentication"); - } - if (connection.authObj.wait) { - connection.authObj.history.push(params); - connection.response(response); - - } else if (response === true) { - this.cmdDoAUTH(connection); - } -} + return connection.response("-ERR [AUTH] Invalid authentication"); + } + if (connection.authObj.wait) { + connection.authObj.history.push(params); + connection.response(response); + + } else if (response === true) { + this.cmdDoAUTH(connection); + } +}; -POP3Server.prototype.cmdAUTHCheck = function (user, passFn) { - if (typeof this.authCallback == "function") { - if (typeof passFn == "function") { - return !!this.authCallback(user, passFn); - } else if (typeof passFn == "string" || typeof passFn == "number") { - return !!this.authCallback(user, function (pass) { - return pass == passFn; - }); - } else { - return false; - } +POP3Server.prototype.cmdAUTHCheck = function(user, passFn) { + if (typeof this.authCallback == "function") { + if (typeof passFn == "function") { + return !!this.authCallback(user, passFn); + } else if (typeof passFn == "string" || typeof passFn == "number") { + return !!this.authCallback(user, function(pass) { + return pass == passFn; + }); + } else { + return false; } - return true; -} + } + return true; +}; // APOP username hash - Performs an APOP authentication // http://www.faqs.org/rfcs/rfc1939.html #7 @@ -365,218 +365,217 @@ POP3Server.prototype.cmdAUTHCheck = function (user, passFn) { // USAGE: // CLIENT: APOP user MD5(salt+pass) // SERVER: +OK You are now logged in -POP3Server.prototype.cmdAPOP = function (connection, params) { - params = params.split(" "); - var self = this; - var user = params[0] && params[0].trim(); - var hash = params[1] && params[1].trim().toLowerCase(); - var salt = "<" + connection.UID + "@" + self.server_name + ">"; - var response; - - function handle() { - self.afterLogin(connection, function(err, isAfterLogin) { - if (err) { - return connection.response("-ERR [SYS] Error with initializing"); - } - if (isAfterLogin !== true) { - return connection.response(response); - } - connection.user = user; - connection.state = States.TRANSACTION; - connection.response("+OK You are now logged in"); - }); - } +POP3Server.prototype.cmdAPOP = function(connection, params) { + params = params.split(" "); + var self = this; + var user = params[0] && params[0].trim(); + var hash = params[1] && params[1].trim().toLowerCase(); + var salt = "<" + connection.UID + "@" + self.server_name + ">"; + var response; + + function handle() { + self.afterLogin(connection, function(err, isAfterLogin) { + if (err) { + return connection.response("-ERR [SYS] Error with initializing"); + } + if (isAfterLogin !== true) { + return connection.response(response); + } + connection.user = user; + connection.state = States.TRANSACTION; + connection.response("+OK You are now logged in"); + }); + } - if (connection.state != States.AUTHENTICATION) { - connection.response("-ERR Only allowed in authentication mode"); - return; - } + if (connection.state != States.AUTHENTICATION) { + connection.response("-ERR Only allowed in authentication mode"); + return; + } - if (typeof self.authCallback == "function") { - self.authCallback(user, function (foundPassword) { - if (md5(salt + foundPassword) != hash) - return connection.response("-ERR [AUTH] Invalid login"); - }) - handle(); - return; - } + if (typeof self.authCallback == "function") { + self.authCallback(user, function(foundPassword) { + if (md5(salt + foundPassword) != hash) + return connection.response("-ERR [AUTH] Invalid login"); + }); handle(); -} + return; + } + handle(); +}; // USER username - Performs basic authentication, PASS follows -POP3Server.prototype.cmdUSER = function (connection, username) { - if (connection.state != States.AUTHENTICATION) { - return connection.response("-ERR Only allowed in authentication mode"); - } +POP3Server.prototype.cmdUSER = function(connection, username) { + if (connection.state != States.AUTHENTICATION) { + return connection.response("-ERR Only allowed in authentication mode"); + } - connection.user = username.trim().toLowerCase(); - if (!connection.user) { - return connection.response("-ERR User not set, try: USER "); - } + connection.user = username.trim().toLowerCase(); + if (!connection.user) { + return connection.response("-ERR User not set, try: USER "); + } - return connection.response("+OK User accepted"); -} + return connection.response("+OK User accepted"); +}; // PASS - Performs basic authentication, runs after USER -POP3Server.prototype.cmdPASS = function (connection, password) { - var self = this; - function handle() { - let response; - self.afterLogin(connection, function(err, response) { - if (err) { - return connection.response("-ERR [SYS] Error with initializing"); - } - if (response === true) { - connection.state = States.TRANSACTION; - return connection.response("+OK You are now logged in"); - } - connection.response(response); - }); - } - - if (connection.state != States.AUTHENTICATION) { - return connection.response("-ERR Only allowed in authentication mode"); - } - if (!connection.user) { - return connection.response("-ERR USER not yet set"); - } - - if (typeof self.authCallback == "function") { - self.authCallback(connection.user, function (foundPassword) { - if (foundPassword !== password) { - connection.response("-ERR [AUTH] Invalid login"); - delete connection.user; - return; - } - handle(); - }); +POP3Server.prototype.cmdPASS = function(connection, password) { + var self = this; + function handle() { + self.afterLogin(connection, function(err, response) { + if (err) { + return connection.response("-ERR [SYS] Error with initializing"); + } + if (response === true) { + connection.state = States.TRANSACTION; + return connection.response("+OK You are now logged in"); + } + connection.response(response); + }); + } + + if (connection.state != States.AUTHENTICATION) { + return connection.response("-ERR Only allowed in authentication mode"); + } + if (!connection.user) { + return connection.response("-ERR USER not yet set"); + } + + if (typeof self.authCallback == "function") { + self.authCallback(connection.user, function(foundPassword) { + if (foundPassword !== password) { + connection.response("-ERR [AUTH] Invalid login"); + delete connection.user; return; - } + } + handle(); + }); + return; + } - handle(); -} + handle(); +}; // TRANSACTION commands // NOOP - always responds with +OK -POP3Server.prototype.cmdNOOP = function (connection) { - if (connection.state != States.TRANSACTION) { - return connection.response("-ERR Only allowed in transaction mode"); - } - connection.response("+OK"); -} +POP3Server.prototype.cmdNOOP = function(connection) { + if (connection.state != States.TRANSACTION) { + return connection.response("-ERR Only allowed in transaction mode"); + } + connection.response("+OK"); +}; // STAT Lists the total count and bytesize of the messages -POP3Server.prototype.cmdSTAT = function (connection) { - if (connection.state != States.TRANSACTION) { - return connection.response("-ERR Only allowed in transaction mode"); +POP3Server.prototype.cmdSTAT = function(connection) { + if (connection.state != States.TRANSACTION) { + return connection.response("-ERR Only allowed in transaction mode"); + } + + connection.store.stat(function(err, length, size) { + if (err) { + connection.response("-ERR STAT failed"); + } else { + connection.response("+OK " + length + " " + size); } - - connection.store.stat(function(err, length, size) { - if (err) { - connection.response("-ERR STAT failed"); - } else { - connection.response("+OK " + length + " " + size); - } - }); + }); }; // LIST [msg] lists all messages -POP3Server.prototype.cmdLIST = function (connection, msg) { - if (connection.state != States.TRANSACTION) { - return connection.response("-ERR Only allowed in transaction mode"); +POP3Server.prototype.cmdLIST = function(connection, msg) { + if (connection.state != States.TRANSACTION) { + return connection.response("-ERR Only allowed in transaction mode"); + } + + connection.store.list(msg, function(err, list) { + if (err) { + debug('LIST failed internally', err); + return connection.response("-ERR LIST command failed"); + } + if (!list) { + debug('LIST failed no message', msg); + return connection.response("-ERR Invalid message ID"); } - connection.store.list(msg, function (err, list) { - if (err) { - debug('LIST failed internally', err); - return connection.response("-ERR LIST command failed") - } - if (!list) { - debug('LIST failed no message', msg); - return connection.response("-ERR Invalid message ID"); - } - - if (typeof list == "string") { - connection.response("+OK " + list); - } else { - connection.response("+OK"); - for (var i = 0; i < list.length; i++) { - connection.response(list[i]); - } - connection.response("."); - } - }); + if (typeof list == "string") { + connection.response("+OK " + list); + } else { + connection.response("+OK"); + for (var i = 0; i < list.length; i++) { + connection.response(list[i]); + } + connection.response("."); + } + }); }; // UIDL - lists unique identifiers for stored messages -POP3Server.prototype.cmdUIDL = function (connection, msg) { - if (connection.state != States.TRANSACTION) { - return connection.response("-ERR Only allowed in transaction mode"); +POP3Server.prototype.cmdUIDL = function(connection, msg) { + if (connection.state != States.TRANSACTION) { + return connection.response("-ERR Only allowed in transaction mode"); + } + + connection.store.uidl(msg, (function(err, list) { + if (err) { + return connection.response("-ERR UIDL command failed"); } - connection.store.uidl(msg, (function (err, list) { - if (err) { - return connection.response("-ERR UIDL command failed") - } - - if (!list) - return connection.response("-ERR Invalid message ID", msg); - - if (typeof list == "string") { - connection.response("+OK " + list); - } else { - connection.response("+OK"); - for (var i = 0; i < list.length; i++) { - connection.response(list[i]); - } - connection.response("."); - } - }).bind(this)); -} + if (!list) + return connection.response("-ERR Invalid message ID", msg); -// RETR msg - outputs a selected message -POP3Server.prototype.cmdRETR = function (connection, msg) { - if (connection.state != States.TRANSACTION) { - return connection.response("-ERR Only allowed in transaction mode"); + if (typeof list == "string") { + connection.response("+OK " + list); + } else { + connection.response("+OK"); + for (var i = 0; i < list.length; i++) { + connection.response(list[i]); + } + connection.response("."); } + }).bind(this)); +}; - connection.store.retr(msg, (function (err, message) { - if (err) { - return connection.response("-ERR RETR command failed") - } - if (!message) { - return connection.response("-ERR Invalid message ID " + msg); - } - connection.response("+OK " + message.length + " octets"); - connection.response(message); - connection.response("."); - }).bind(this)); +// RETR msg - outputs a selected message +POP3Server.prototype.cmdRETR = function(connection, msg) { + if (connection.state != States.TRANSACTION) { + return connection.response("-ERR Only allowed in transaction mode"); + } -} + connection.store.retr(msg, (function(err, message) { + if (err) { + return connection.response("-ERR RETR command failed"); + } + if (!message) { + return connection.response("-ERR Invalid message ID " + msg); + } + connection.response("+OK " + message.length + " octets"); + connection.response(message); + connection.response("."); + }).bind(this)); + +}; // DELE msg - marks selected message for deletion -POP3Server.prototype.cmdDELE = function (connection, msg) { - if (connection.state != States.TRANSACTION) { - return connection.response("-ERR Only allowed in transaction mode"); - } +POP3Server.prototype.cmdDELE = function(connection, msg) { + if (connection.state != States.TRANSACTION) { + return connection.response("-ERR Only allowed in transaction mode"); + } - connection.store.dele(msg, (function (err, success) { - if (err) { - return connection.response("-ERR RETR command failed") - } - if (!success) { - return connection.response("-ERR Invalid message ID " + msg); - } else { - connection.response("+OK msg deleted"); - } - }).bind(this)); + connection.store.dele(msg, (function(err, success) { + if (err) { + return connection.response("-ERR RETR command failed"); + } + if (!success) { + return connection.response("-ERR Invalid message ID " + msg); + } else { + connection.response("+OK msg deleted"); + } + }).bind(this)); -} +}; // RSET - resets DELE'ted message flags -POP3Server.prototype.cmdRSET = function (connection) { - if (connection.state != States.TRANSACTION) return connection.response("-ERR Only allowed in transaction mode"); - connection.store.rset(); - connection.response("+OK"); -} +POP3Server.prototype.cmdRSET = function(connection) { + if (connection.state != States.TRANSACTION) return connection.response("-ERR Only allowed in transaction mode"); + connection.store.rset(); + connection.response("+OK"); +}; diff --git a/states.js b/states.js index 46385b9..575b837 100644 --- a/states.js +++ b/states.js @@ -4,7 +4,7 @@ * @type {object} **/ module.exports = { - AUTHENTICATION: 1, - TRANSACTION: 2, - UPDATE: 3 + AUTHENTICATION: 1, + TRANSACTION: 2, + UPDATE: 3 }; diff --git a/tests/index.js b/test/index.js similarity index 98% rename from tests/index.js rename to test/index.js index 91e1d7b..853878d 100644 --- a/tests/index.js +++ b/test/index.js @@ -5,7 +5,7 @@ var path = require('path'); var POP3Client = require('mailx/lib/poplib'); var POP3Server = require('../'); var mailx = require('mailx'); -var mailcomposer = require("mailcomposer"); +var mailcomposer = require("nodemailer/lib/mail-composer"); const PORT = 11000; const SSL_PORT = 9930; @@ -46,8 +46,9 @@ describe('POP3 server', function(){ message.addTo('you', 'you@example.net'); message.setSubject('hello'); message.setText('hi ! how are u?'); - message.setHtml('hi ! how are u? hugs'); - mailcomposer(message).build(cb); + message.setHtml('hi ! how are u? hugs'); + var mail = new mailcomposer(message); + mail.compile().build(cb); }, removeDeleted: function(deleted, cb) { deleted.forEach(function(uid) {