mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-09-06 21:24:37 +08:00
implemented list handling pop3 commands (no updates yet)
This commit is contained in:
parent
3ddc3d7d62
commit
6f615127b6
3 changed files with 303 additions and 32 deletions
|
@ -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');
|
||||
|
||||
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');
|
||||
|
||||
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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
48
pop3.js
48
pop3.js
|
@ -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) {
|
||||
|
|
Loading…
Add table
Reference in a new issue