implemented list handling pop3 commands (no updates yet)

This commit is contained in:
Andris Reinman 2017-04-08 12:39:07 +03:00
parent 3ddc3d7d62
commit 6f615127b6
3 changed files with 303 additions and 32 deletions

View file

@ -30,7 +30,24 @@ class POP3Connection extends EventEmitter {
cid: this._id,
host: this.remoteAddress
}, 'Connection from %s', this.remoteAddress);
this._socket.write('+OK WDPop ready for requests from ' + this.remoteAddress + '\r\n');
this.send('+OK WDPop ready for requests from ' + this.remoteAddress);
}
send(payload) {
if (!this._socket || !this._socket.writable) {
return;
}
if (Array.isArray(payload)) {
payload = payload.join('\r\n') + '\r\n.';
}
this._server.logger.debug({
tnx: 'send',
cid: this._id,
host: this.remoteAddress
}, 'S:', payload);
this._socket.write(payload + '\r\n');
}
_setListeners() {
@ -67,7 +84,7 @@ class POP3Connection extends EventEmitter {
tnx: 'close',
cid: this._id,
host: this.remoteAddress,
user: this.user
user: this.session.user && this.session.user.username
}, 'Connection closed to %s', this.remoteAddress);
this.emit('close');
@ -87,7 +104,7 @@ class POP3Connection extends EventEmitter {
this._server.logger.error({
err,
tnx: 'error',
user: this.user
user: this.session.user && this.session.user.username
}, '%s', err.message);
this.emit('error', err);
}
@ -98,6 +115,7 @@ class POP3Connection extends EventEmitter {
* @event
*/
_onTimeout() {
this.send('-ERR Disconnected for inactivity');
this.close();
}
@ -148,6 +166,12 @@ class POP3Connection extends EventEmitter {
let command = parts.shift().toUpperCase();
let args = parts.join(' ');
this._server.logger.debug({
tnx: 'receive',
cid: this._id,
user: this.session.user && this.session.user.username
}, 'C:', (line || '').toString());
if (typeof this['command_' + command] === 'function') {
this['command_' + command](args, err => {
if (err) {
@ -158,14 +182,14 @@ class POP3Connection extends EventEmitter {
cid: this._id,
host: this.remoteAddress
}, 'Error running %s. %s', command, err.message);
this._socket.write('-ERR ' + err.message + '\r\n');
this.send('-ERR ' + err.message);
this.close();
} else {
this.processQueue();
}
});
} else {
this._socket.write('-ERR bad command\r\n');
this.send('-ERR bad command');
this.close();
}
}
@ -181,30 +205,29 @@ class POP3Connection extends EventEmitter {
'SASL PLAIN'
];
this._socket.write('+OK Capability list follows\r\n' +
extensions.join('\r\n') + '\r\n.\r\n');
this.send(['+OK Capability list follows'].concat(extensions));
next();
}
command_USER(args, next) {
if (this.session.state !== 'AUTHORIZATION') {
this._socket.write('-ERR Command not accepted\r\n');
this.send('-ERR Command not accepted');
return next();
}
if (!args) {
this._socket.write('-ERR USER who?\r\n');
this.send('-ERR USER who?');
return next();
}
this.session.user = args;
this._socket.write('+OK send PASS\r\n');
this.send('+OK send PASS');
return next();
}
command_PASS(args, next) {
if (this.session.state !== 'AUTHORIZATION') {
this._socket.write('-ERR Command not accepted\r\n');
this.send('-ERR Command not accepted');
return next();
}
@ -240,7 +263,7 @@ class POP3Connection extends EventEmitter {
method: 'USER',
user: username
}, 'Authentication failed for %s using %s', username, 'USER');
this._socket.write('-ERR [AUTH] ' + (response.message || 'Username and password not accepted.') + '\r\n');
this.send('-ERR [AUTH] ' + (response.message || 'Username and password not accepted'));
return next();
}
@ -251,15 +274,19 @@ class POP3Connection extends EventEmitter {
user: username
}, '%s authenticated using %s', username, 'USER');
this.session.user = response.user;
this.session.state = 'TRANSACTION';
this._socket.write('+OK Welcome.\r\n');
next();
this.openMailbox(err => {
if (err) {
return next(err);
}
next();
});
});
}
command_AUTH(args, next) {
if (this.session.state !== 'AUTHORIZATION') {
this._socket.write('-ERR Command not accepted\r\n');
this.send('-ERR Command not accepted');
return next();
}
@ -268,18 +295,18 @@ class POP3Connection extends EventEmitter {
let plain = params.shift();
if (mechanism !== 'PLAIN') {
this._socket.write('-ERR unsupported SASL mechanism\r\n');
this.send('-ERR unsupported SASL mechanism');
return next();
}
if (params.length || !/^[a-zA-Z0-9+\/]+=+?$/.test(plain)) {
this._socket.write('-ERR malformed command\r\n');
this.send('-ERR malformed command');
return next();
}
let credentials = Buffer.from(plain, 'base64').toString().split('\x00');
if (credentials.length !== 3) {
this._socket.write('-ERR malformed command\r\n');
this.send('-ERR malformed command');
return next();
}
@ -310,7 +337,7 @@ class POP3Connection extends EventEmitter {
method: 'PLAIN',
user: username
}, 'Authentication failed for %s using %s', username, 'PLAIN');
this._socket.write('-ERR [AUTH] ' + (response.message || 'Username and password not accepted.') + '\r\n');
this.send('-ERR [AUTH] ' + (response.message || 'Username and password not accepted'));
return next();
}
@ -321,18 +348,22 @@ class POP3Connection extends EventEmitter {
user: username
}, '%s authenticated using %s', username, 'PLAIN');
this.session.user = response.user;
this.session.state = 'TRANSACTION';
this._socket.write('+OK Welcome.\r\n');
next();
this.openMailbox(err => {
if (err) {
return next(err);
}
next();
});
});
}
// https://tools.ietf.org/html/rfc1939#page-9
command_NOOP(args, next) {
if (this.session.state !== 'TRANSACTION') {
this._socket.write('-ERR Command not accepted\r\n');
this.send('-ERR Command not accepted');
} else {
this._socket.write('+OK\r\n');
this.send('+OK');
}
return next();
}
@ -340,7 +371,7 @@ class POP3Connection extends EventEmitter {
// https://tools.ietf.org/html/rfc1939#section-6
command_QUIT() {
let finish = () => {
this._socket.write('+OK Bye\r\n');
this.send('+OK Bye');
this.close();
};
@ -351,6 +382,190 @@ class POP3Connection extends EventEmitter {
// TODO: run pending actions
finish();
}
// https://tools.ietf.org/html/rfc1939#page-6
command_STAT(args, next) {
if (this.session.state !== 'TRANSACTION') {
this.send('-ERR Command not accepted');
} else {
this.send('+OK ' + this.session.listing.count + ' ' + this.session.listing.size);
}
return next();
}
// https://tools.ietf.org/html/rfc1939#page-6
command_LIST(args, next) {
if (this.session.state !== 'TRANSACTION') {
this.send('-ERR Command not accepted');
return next();
}
let index = false;
if (args) {
index = Number(args);
}
if (args && (isNaN(index) || index <= 0)) {
return next(new Error('malformed command'));
}
if (args && index > this.session.listing.messages.length) {
this.send('-ERR no such message, only ' + this.session.listing.messages.length + ' messages in maildrop');
return next();
}
if (index) {
this.send('+OK ' + index + ' ' + this.session.listing.messages[index - 1].size);
} else {
this.send(
['+OK ' + this.session.listing.count + ' ' + this.session.listing.size]
.concat(
this.session.listing.messages
.filter(message => !message.popped)
.map((message, i) => (i + 1) + ' ' + message.size)
));
}
return next();
}
// https://tools.ietf.org/html/rfc1939#page-12
command_UIDL(args, next) {
if (this.session.state !== 'TRANSACTION') {
this.send('-ERR Command not accepted');
return next();
}
let index = false;
if (args) {
index = Number(args);
}
if (args && (isNaN(index) || index <= 0)) {
return next(new Error('malformed command'));
}
if (args && index > this.session.listing.messages.length) {
this.send('-ERR no such message, only ' + this.session.listing.messages.length + ' messages in maildrop');
return next();
}
if (index) {
this.send('+OK ' + index + ' ' + this.session.listing.messages[index - 1].id);
} else {
this.send(
['+OK']
.concat(
this.session.listing.messages
.filter(message => !message.popped)
.map((message, i) => (i + 1) + ' ' + message.id)
));
}
return next();
}
// https://tools.ietf.org/html/rfc1939#page-8
command_DELE(args, next) {
if (this.session.state !== 'TRANSACTION') {
this.send('-ERR Command not accepted');
return next();
}
let index = false;
if (args) {
index = Number(args);
}
if (!args || isNaN(index) || index <= 0) {
return next(new Error('malformed command'));
}
if (args && index > this.session.listing.messages.length) {
this.send('-ERR no such message, only ' + this.session.listing.messages.length + ' messages in maildrop');
return next();
}
let message = this.session.listing.messages[index - 1];
if (message.popped) {
this.send('-ERR message ' + index + ' already deleted');
return next();
}
message.popped = true;
this.session.listing.count--;
this.session.listing.size -= message.size;
this.send('+OK message ' + index + ' deleted');
return next();
}
// https://tools.ietf.org/html/rfc1939#page-9
command_RSET(args, next) {
if (this.session.state !== 'TRANSACTION') {
this.send('-ERR Command not accepted');
return next();
}
let count = 0;
let size = 0;
this.session.listing.messages.forEach(message => {
if (message.popped) {
message.popped = false;
count++;
size += message.size;
}
});
this.session.listing.count += count;
this.session.listing.size += size;
this.send('+OK maildrop has ' + this.session.listing.count + ' message' + (this.session.listing.count !== 1 ? 's' : '') + ' (' + this.session.listing.size + ' octets)');
return next();
}
// https://tools.ietf.org/html/rfc1939#page-8
command_RETR(args, next) {
if (this.session.state !== 'TRANSACTION') {
this.send('-ERR Command not accepted');
}
this.send('-ERR Future feature');
return next();
}
// https://tools.ietf.org/html/rfc1939#page-11
command_TOP(args, next) {
if (this.session.state !== 'TRANSACTION') {
this.send('-ERR Command not accepted');
}
this.send('-ERR Future feature');
return next();
}
openMailbox(next) {
this._server.onListMessages(this.session, (err, listing) => {
if (err) {
this._server.logger.info({
err,
tnx: 'listerr',
cid: this._id,
user: this.session.user && this.session.user.username
}, 'Failed listing messages for %s. %s', this.session.user.username, err.message);
return next(err);
}
this.session.listing = listing;
this.session.state = 'TRANSACTION';
this.send('+OK maildrop has ' + listing.count + ' message' + (listing.count !== 1 ? 's' : '') + ' (' + listing.size + ' octets)');
return next();
});
}
}
module.exports = POP3Connection;

View file

@ -16,7 +16,7 @@ class POP3Server extends EventEmitter {
this.options = options || {};
// apply shorthand handlers
['onAuth'].forEach(handler => {
['onAuth', 'onListMessages'].forEach(handler => {
if (typeof this.options[handler] === 'function') {
this[handler] = this.options[handler];
}
@ -150,6 +150,7 @@ class POP3Server extends EventEmitter {
* Authentication handler. Override this
*
* @param {Object} auth Authentication options
* @param {Object} session Session object
* @param {Function} callback Callback to run once the user is authenticated
*/
onAuth(auth, session, callback) {
@ -158,6 +159,21 @@ class POP3Server extends EventEmitter {
});
}
/**
* Message listing handler. Override this
*
* @param {Object} session Session object
* @param {Function} callback Callback to run with message listing
*/
onListMessages(session, callback) {
// messages are objects {id: 'abc', size: 123}
return callback(null, {
messages: [],
count: 0,
size: 0
});
}
listen(...args) {
this.server.listen(...args);
}

50
pop3.js
View file

@ -7,6 +7,8 @@ const fs = require('fs');
const bcrypt = require('bcryptjs');
const db = require('./lib/db');
const MAX_MESSAGES = 5000;
const serverOptions = {
port: config.pop3.port,
host: config.pop3.host,
@ -28,29 +30,67 @@ const serverOptions = {
}
},
onAuth(auth, session, next) {
onAuth(auth, session, callback) {
db.database.collection('users').findOne({
username: auth.username
}, (err, user) => {
if (err) {
return next(err);
return callback(err);
}
if (!user || !bcrypt.compareSync(auth.password, user.password)) {
return next(null, {
return callback(null, {
message: 'Authentication failed'
});
}
next(null, {
callback(null, {
user: {
id: user._id,
username: user.username
}
});
});
}
},
onListMessages(session, callback) {
// only list messages in INBOX
db.database.collection('mailboxes').findOne({
user: session.user.id,
path: 'INBOX'
}, (err, mailbox) => {
if (err) {
return callback(err);
}
if (!mailbox) {
return callback(new Error('Mailbox not found for user'));
}
db.database.collection('messages').find({
mailbox: mailbox._id
}).project({
uid: true,
size: true
}).sort([
['uid', -1]
]).limit(MAX_MESSAGES).toArray((err, messages) => {
if (err) {
return callback(err);
}
return callback(null, {
messages: messages.map(message => ({
id: message._id.toString(),
size: message.size
})),
count: messages.length,
size: messages.reduce((acc, message) => acc + message.size, 0)
});
});
});
}
};
if (config.pop3.key) {