2017-04-08 02:29:14 +08:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
const crypto = require('crypto');
|
|
|
|
const EventEmitter = require('events');
|
2017-04-09 17:33:10 +08:00
|
|
|
const packageData = require('../package.json');
|
2017-04-10 04:21:36 +08:00
|
|
|
const DataStream = require('nodemailer/lib/smtp-connection/data-stream');
|
2017-04-08 02:29:14 +08:00
|
|
|
|
|
|
|
const SOCKET_TIMEOUT = 60 * 1000;
|
|
|
|
|
|
|
|
class POP3Connection extends EventEmitter {
|
|
|
|
constructor(server, socket) {
|
|
|
|
super();
|
|
|
|
this._server = server;
|
|
|
|
this._socket = socket;
|
|
|
|
|
|
|
|
this._closed = false;
|
|
|
|
this._closing = false;
|
|
|
|
|
|
|
|
this.remoteAddress = this._socket.remoteAddress;
|
|
|
|
this._id = crypto.randomBytes(9).toString('base64');
|
|
|
|
|
|
|
|
this.processing = false;
|
|
|
|
this.queue = [];
|
|
|
|
this._remainder = '';
|
|
|
|
}
|
|
|
|
|
|
|
|
init() {
|
|
|
|
this._setListeners();
|
|
|
|
this._resetSession();
|
|
|
|
this._server.logger.info({
|
|
|
|
tnx: 'connection',
|
|
|
|
cid: this._id,
|
|
|
|
host: this.remoteAddress
|
|
|
|
}, 'Connection from %s', this.remoteAddress);
|
2017-04-08 17:39:07 +08:00
|
|
|
this.send('+OK WDPop ready for requests from ' + this.remoteAddress);
|
|
|
|
}
|
|
|
|
|
2017-04-10 04:21:36 +08:00
|
|
|
write(payload) {
|
|
|
|
if (!this._socket || !this._socket.writable) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this._socket.write(payload);
|
|
|
|
}
|
|
|
|
|
2017-04-08 17:39:07 +08:00
|
|
|
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
|
2017-04-13 03:04:57 +08:00
|
|
|
}, 'S:', (payload.length < 128 ? payload : payload.substr(0, 128) + '... +' + (payload.length - 128) + ' B').replace(/\r?\n/g,'\\n'));
|
2017-04-10 04:21:36 +08:00
|
|
|
this.write(payload + '\r\n');
|
2017-04-08 02:29:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
_setListeners() {
|
|
|
|
this._socket.on('close', () => this._onClose());
|
|
|
|
this._socket.on('error', err => this._onError(err));
|
|
|
|
this._socket.setTimeout(this._server.options.socketTimeout || SOCKET_TIMEOUT, () => this._onTimeout());
|
|
|
|
this._socket.on('readable', () => {
|
|
|
|
if (this.processing) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.processing = true;
|
|
|
|
|
|
|
|
this.read();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fired when the socket is closed
|
|
|
|
* @event
|
|
|
|
*/
|
|
|
|
_onClose( /* hadError */ ) {
|
|
|
|
if (this._closed) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.queue = [];
|
|
|
|
this.processing = false;
|
|
|
|
this._remainder = '';
|
|
|
|
|
|
|
|
this._closed = true;
|
|
|
|
this._closing = false;
|
|
|
|
|
|
|
|
this._server.logger.info({
|
|
|
|
tnx: 'close',
|
|
|
|
cid: this._id,
|
|
|
|
host: this.remoteAddress,
|
2017-04-08 17:39:07 +08:00
|
|
|
user: this.session.user && this.session.user.username
|
2017-04-08 02:29:14 +08:00
|
|
|
}, 'Connection closed to %s', this.remoteAddress);
|
|
|
|
|
|
|
|
this.emit('close');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fired when an error occurs with the socket
|
|
|
|
*
|
|
|
|
* @event
|
|
|
|
* @param {Error} err Error object
|
|
|
|
*/
|
|
|
|
_onError(err) {
|
|
|
|
if (err.code === 'ECONNRESET' || err.code === 'EPIPE') {
|
|
|
|
return this.close(); // mark connection as 'closing'
|
|
|
|
}
|
|
|
|
|
|
|
|
this._server.logger.error({
|
|
|
|
err,
|
|
|
|
tnx: 'error',
|
2017-04-08 17:39:07 +08:00
|
|
|
user: this.session.user && this.session.user.username
|
2017-04-08 02:29:14 +08:00
|
|
|
}, '%s', err.message);
|
|
|
|
this.emit('error', err);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fired when socket timeouts. Closes connection
|
|
|
|
*
|
|
|
|
* @event
|
|
|
|
*/
|
|
|
|
_onTimeout() {
|
2017-04-08 17:39:07 +08:00
|
|
|
this.send('-ERR Disconnected for inactivity');
|
2017-04-08 02:29:14 +08:00
|
|
|
this.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
_resetSession() {
|
2017-04-08 03:38:52 +08:00
|
|
|
this.session = {
|
|
|
|
state: 'AUTHORIZATION'
|
|
|
|
};
|
2017-04-08 02:29:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
close() {
|
|
|
|
if (!this._socket.destroyed && this._socket.writable) {
|
|
|
|
this._socket.end();
|
|
|
|
}
|
|
|
|
this._closing = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
read() {
|
|
|
|
let chunk;
|
|
|
|
let data = this._remainder;
|
|
|
|
while ((chunk = this._socket.read()) !== null) {
|
|
|
|
data += chunk.toString('binary');
|
|
|
|
if (data.indexOf('\n') >= 0) {
|
|
|
|
let lines = data.split(/\r?\n/).map(line => Buffer.from(line, 'binary').toString());
|
|
|
|
this._remainder = lines.pop();
|
|
|
|
|
|
|
|
if (lines.length) {
|
|
|
|
if (this.queue.length) {
|
|
|
|
this.queue = this.queue.concat(lines);
|
|
|
|
} else {
|
|
|
|
this.queue = lines;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.processQueue();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.processing = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
processQueue() {
|
|
|
|
if (!this.queue.length) {
|
|
|
|
this.read(); // see if there's anything left to read
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let line = this.queue.shift().trim();
|
2017-04-10 04:40:16 +08:00
|
|
|
|
|
|
|
if (typeof this._nextHandler === 'function') {
|
|
|
|
let handler = this._nextHandler;
|
|
|
|
this._nextHandler = null;
|
|
|
|
this._server.logger.debug({
|
|
|
|
tnx: 'receive',
|
|
|
|
cid: this._id,
|
|
|
|
user: this.session.user && this.session.user.username
|
|
|
|
}, 'C: <%s bytes of continue data>', Buffer.byteLength(line));
|
|
|
|
return handler(line, err => {
|
|
|
|
if (err) {
|
|
|
|
this._server.logger.info({
|
|
|
|
err,
|
|
|
|
tnx: '+',
|
|
|
|
cid: this._id,
|
|
|
|
host: this.remoteAddress
|
|
|
|
}, 'Error processing continue data. %s', err.message);
|
|
|
|
this.send('-ERR ' + err.message);
|
|
|
|
this.close();
|
|
|
|
} else {
|
|
|
|
this.processQueue();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-04-08 02:29:14 +08:00
|
|
|
let parts = line.split(' ');
|
|
|
|
let command = parts.shift().toUpperCase();
|
|
|
|
let args = parts.join(' ');
|
|
|
|
|
2017-04-10 04:40:16 +08:00
|
|
|
let logLine = (line || '').toString();
|
|
|
|
if (/^(PASS|AUTH PLAIN)\s+[^\s]+/i.test(line)) {
|
|
|
|
logLine = logLine.replace(/[^\s]+$/, '*hidden*');
|
|
|
|
}
|
2017-04-08 17:39:07 +08:00
|
|
|
this._server.logger.debug({
|
|
|
|
tnx: 'receive',
|
|
|
|
cid: this._id,
|
|
|
|
user: this.session.user && this.session.user.username
|
2017-04-10 04:40:16 +08:00
|
|
|
}, 'C:', logLine);
|
2017-04-08 17:39:07 +08:00
|
|
|
|
2017-04-08 03:38:52 +08:00
|
|
|
if (typeof this['command_' + command] === 'function') {
|
|
|
|
this['command_' + command](args, err => {
|
|
|
|
if (err) {
|
|
|
|
this._server.logger.info({
|
|
|
|
err,
|
|
|
|
tnx: 'command',
|
|
|
|
command,
|
|
|
|
cid: this._id,
|
|
|
|
host: this.remoteAddress
|
|
|
|
}, 'Error running %s. %s', command, err.message);
|
2017-04-08 17:39:07 +08:00
|
|
|
this.send('-ERR ' + err.message);
|
2017-04-08 03:38:52 +08:00
|
|
|
this.close();
|
|
|
|
} else {
|
|
|
|
this.processQueue();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
2017-04-08 17:39:07 +08:00
|
|
|
this.send('-ERR bad command');
|
2017-04-08 03:38:52 +08:00
|
|
|
this.close();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// https://tools.ietf.org/html/rfc2449#section-5
|
|
|
|
command_CAPA(args, next) {
|
|
|
|
let extensions = [
|
2017-04-09 17:33:10 +08:00
|
|
|
'CAPA',
|
2017-04-10 04:21:36 +08:00
|
|
|
'TOP',
|
2017-04-08 03:38:52 +08:00
|
|
|
'UIDL',
|
|
|
|
'USER',
|
|
|
|
'RESP-CODES',
|
|
|
|
// https://tools.ietf.org/html/rfc5034#section-6
|
2017-04-09 17:33:10 +08:00
|
|
|
'SASL PLAIN',
|
|
|
|
// https://tools.ietf.org/html/rfc2449#section-6.6
|
|
|
|
'PIPELINING',
|
|
|
|
'IMPLEMENTATION WildDuck-v' + packageData.version
|
2017-04-08 03:38:52 +08:00
|
|
|
];
|
|
|
|
|
2017-04-08 17:39:07 +08:00
|
|
|
this.send(['+OK Capability list follows'].concat(extensions));
|
2017-04-08 03:38:52 +08:00
|
|
|
|
|
|
|
next();
|
|
|
|
}
|
|
|
|
|
|
|
|
command_USER(args, next) {
|
|
|
|
if (this.session.state !== 'AUTHORIZATION') {
|
2017-04-08 17:39:07 +08:00
|
|
|
this.send('-ERR Command not accepted');
|
2017-04-08 03:38:52 +08:00
|
|
|
return next();
|
|
|
|
}
|
|
|
|
if (!args) {
|
2017-04-08 17:39:07 +08:00
|
|
|
this.send('-ERR USER who?');
|
2017-04-08 03:38:52 +08:00
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.session.user = args;
|
2017-04-08 17:39:07 +08:00
|
|
|
this.send('+OK send PASS');
|
2017-04-08 03:38:52 +08:00
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
command_PASS(args, next) {
|
|
|
|
if (this.session.state !== 'AUTHORIZATION') {
|
2017-04-08 17:39:07 +08:00
|
|
|
this.send('-ERR Command not accepted');
|
2017-04-08 03:38:52 +08:00
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.session.user || !args) {
|
|
|
|
return next(new Error('malformed command'));
|
|
|
|
}
|
|
|
|
|
|
|
|
let username = this.session.user;
|
|
|
|
let password = args;
|
|
|
|
this.session.user = false;
|
|
|
|
|
|
|
|
this._server.onAuth({
|
|
|
|
method: 'USER',
|
|
|
|
username,
|
|
|
|
password
|
|
|
|
}, this.session, (err, response) => {
|
|
|
|
|
|
|
|
if (err) {
|
|
|
|
this._server.logger.info({
|
|
|
|
err,
|
|
|
|
tnx: 'autherror',
|
|
|
|
cid: this._id,
|
|
|
|
method: 'USER',
|
|
|
|
user: username
|
|
|
|
}, 'Authentication error for %s using %s. %s', username, 'USER', err.message);
|
|
|
|
return next(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!response.user) {
|
|
|
|
this._server.logger.info({
|
|
|
|
tnx: 'authfail',
|
|
|
|
cid: this._id,
|
|
|
|
method: 'USER',
|
|
|
|
user: username
|
|
|
|
}, 'Authentication failed for %s using %s', username, 'USER');
|
2017-04-08 17:39:07 +08:00
|
|
|
this.send('-ERR [AUTH] ' + (response.message || 'Username and password not accepted'));
|
2017-04-08 03:38:52 +08:00
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
this._server.logger.info({
|
|
|
|
tnx: 'auth',
|
|
|
|
cid: this._id,
|
|
|
|
method: 'USER',
|
|
|
|
user: username
|
|
|
|
}, '%s authenticated using %s', username, 'USER');
|
|
|
|
this.session.user = response.user;
|
2017-04-08 17:39:07 +08:00
|
|
|
|
|
|
|
this.openMailbox(err => {
|
|
|
|
if (err) {
|
|
|
|
return next(err);
|
|
|
|
}
|
|
|
|
next();
|
|
|
|
});
|
2017-04-08 02:29:14 +08:00
|
|
|
});
|
2017-04-08 03:38:52 +08:00
|
|
|
}
|
2017-04-08 02:29:14 +08:00
|
|
|
|
2017-04-08 03:38:52 +08:00
|
|
|
command_AUTH(args, next) {
|
|
|
|
if (this.session.state !== 'AUTHORIZATION') {
|
2017-04-08 17:39:07 +08:00
|
|
|
this.send('-ERR Command not accepted');
|
2017-04-08 03:38:52 +08:00
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-04-10 04:21:36 +08:00
|
|
|
if (!args) {
|
|
|
|
this.send(['+OK', 'PLAIN']);
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-04-08 03:38:52 +08:00
|
|
|
let params = args.split(/\s+/);
|
|
|
|
let mechanism = params.shift().toUpperCase();
|
|
|
|
|
|
|
|
if (mechanism !== 'PLAIN') {
|
2017-04-08 17:39:07 +08:00
|
|
|
this.send('-ERR unsupported SASL mechanism');
|
2017-04-08 03:38:52 +08:00
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-04-10 04:40:16 +08:00
|
|
|
if (!params.length) {
|
|
|
|
this.send('+');
|
|
|
|
this._nextHandler = (args, next) => this.authPlain(args, next);
|
2017-04-08 03:38:52 +08:00
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-04-10 04:40:16 +08:00
|
|
|
let plain = params.shift();
|
|
|
|
|
|
|
|
if (params.length) {
|
2017-04-08 17:39:07 +08:00
|
|
|
this.send('-ERR malformed command');
|
2017-04-08 03:38:52 +08:00
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-04-10 04:40:16 +08:00
|
|
|
this.authPlain(plain, next);
|
2017-04-08 03:38:52 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// https://tools.ietf.org/html/rfc1939#page-9
|
|
|
|
command_NOOP(args, next) {
|
|
|
|
if (this.session.state !== 'TRANSACTION') {
|
2017-04-08 17:39:07 +08:00
|
|
|
this.send('-ERR Command not accepted');
|
2017-04-08 03:38:52 +08:00
|
|
|
} else {
|
2017-04-08 17:39:07 +08:00
|
|
|
this.send('+OK');
|
2017-04-08 03:38:52 +08:00
|
|
|
}
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
// https://tools.ietf.org/html/rfc1939#section-6
|
|
|
|
command_QUIT() {
|
|
|
|
let finish = () => {
|
2017-04-09 06:18:31 +08:00
|
|
|
this.session = false;
|
2017-04-08 17:39:07 +08:00
|
|
|
this.send('+OK Bye');
|
2017-04-08 03:38:52 +08:00
|
|
|
this.close();
|
|
|
|
};
|
|
|
|
|
|
|
|
if (this.session.state !== 'TRANSACTION') {
|
|
|
|
return finish();
|
|
|
|
}
|
|
|
|
this.session.state = 'UPDATE';
|
2017-04-09 06:18:31 +08:00
|
|
|
|
|
|
|
let deleted = this.session.listing.messages.filter(message => message.popped);
|
|
|
|
let seen = this.session.listing.messages.filter(message => !message.seen && message.fetched && !message.popped);
|
|
|
|
|
|
|
|
if (!deleted.length && !seen.length) {
|
|
|
|
return finish();
|
|
|
|
}
|
|
|
|
|
|
|
|
this._server.onUpdate({
|
|
|
|
deleted,
|
|
|
|
seen
|
|
|
|
}, this.session, finish);
|
2017-04-08 02:29:14 +08:00
|
|
|
}
|
2017-04-08 17:39:07 +08:00
|
|
|
|
|
|
|
// 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');
|
2017-04-09 06:18:31 +08:00
|
|
|
return next();
|
2017-04-08 17:39:07 +08:00
|
|
|
}
|
2017-04-09 06:18:31 +08:00
|
|
|
|
|
|
|
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];
|
|
|
|
|
2017-04-09 17:33:10 +08:00
|
|
|
if (message.popped) {
|
|
|
|
this.send('-ERR message ' + index + ' already deleted');
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-04-09 06:18:31 +08:00
|
|
|
this._server.onFetchMessage(message.id, this.session, (err, stream) => {
|
|
|
|
if (err) {
|
|
|
|
return next(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!stream) {
|
|
|
|
return next(new Error('Can not find message'));
|
|
|
|
}
|
|
|
|
|
|
|
|
stream.once('error', err => next(err));
|
|
|
|
stream.once('end', () => {
|
2017-04-10 04:21:36 +08:00
|
|
|
// this.send('.'); // final dot is sent by DataStream
|
2017-04-09 06:18:31 +08:00
|
|
|
message.fetched = true;
|
|
|
|
return next();
|
|
|
|
});
|
|
|
|
|
|
|
|
this.send('+OK ' + message.size + ' octets');
|
2017-04-10 04:21:36 +08:00
|
|
|
stream.pipe(new DataStream()).pipe(this._socket, {
|
2017-04-09 06:18:31 +08:00
|
|
|
end: false
|
|
|
|
});
|
|
|
|
});
|
2017-04-08 17:39:07 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// https://tools.ietf.org/html/rfc1939#page-11
|
|
|
|
command_TOP(args, next) {
|
|
|
|
if (this.session.state !== 'TRANSACTION') {
|
|
|
|
this.send('-ERR Command not accepted');
|
2017-04-10 04:21:36 +08:00
|
|
|
return next();
|
2017-04-08 17:39:07 +08:00
|
|
|
}
|
2017-04-10 04:21:36 +08:00
|
|
|
|
|
|
|
let parts = args.split(' ');
|
|
|
|
let index = Number(parts[0]);
|
|
|
|
let lines = Number(parts[1]);
|
|
|
|
|
|
|
|
if (!args || parts.length !== 2 || isNaN(index) || index <= 0 || isNaN(lines) || lines < 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();
|
|
|
|
}
|
|
|
|
|
|
|
|
this._server.onFetchMessage(message.id, this.session, (err, stream) => {
|
|
|
|
if (err) {
|
|
|
|
return next(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!stream) {
|
|
|
|
return next(new Error('Can not find message'));
|
|
|
|
}
|
|
|
|
|
|
|
|
let data = '';
|
|
|
|
let headers = false;
|
|
|
|
let finished = false;
|
|
|
|
let linesSent = 0;
|
|
|
|
stream.on('readable', () => {
|
|
|
|
let chunk;
|
|
|
|
while ((chunk = stream.read()) !== null) {
|
|
|
|
if (!finished) {
|
|
|
|
data += chunk.toString('binary');
|
|
|
|
if (!headers) {
|
|
|
|
let match;
|
|
|
|
if ((match = data.match(/\r?\n\r?\n/))) {
|
|
|
|
headers = data.substr(0, match.index + match[0].length);
|
|
|
|
if (data.length > headers.length) {
|
|
|
|
data = data.substr(headers.length);
|
|
|
|
} else {
|
|
|
|
data = '';
|
|
|
|
}
|
2017-04-10 04:40:16 +08:00
|
|
|
this.write(Buffer.from(headers.replace(/^\./gm, '..'), 'binary'));
|
2017-04-10 04:21:36 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (headers) {
|
|
|
|
if (linesSent >= lines) {
|
|
|
|
finished = true;
|
|
|
|
if (typeof stream.abort === 'function') {
|
|
|
|
stream.abort();
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
let match;
|
|
|
|
while (!finished && (match = data.match(/\r?\n/))) {
|
|
|
|
let line = data.substr(0, match.index + match[0].length);
|
|
|
|
linesSent++;
|
|
|
|
if (data.length > line.length) {
|
|
|
|
data = data.substr(line.length);
|
|
|
|
} else {
|
|
|
|
data = '';
|
|
|
|
}
|
2017-04-10 04:40:16 +08:00
|
|
|
this.write(Buffer.from(line.replace(/^\./gm, '..'), 'binary'));
|
2017-04-10 04:21:36 +08:00
|
|
|
if (linesSent >= lines) {
|
|
|
|
finished = true;
|
|
|
|
if (typeof stream.abort === 'function') {
|
|
|
|
stream.abort();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
stream.once('error', err => next(err));
|
|
|
|
stream.once('end', () => {
|
|
|
|
this.send('.');
|
|
|
|
return next();
|
|
|
|
});
|
|
|
|
|
|
|
|
this.send('+OK message follows');
|
|
|
|
});
|
2017-04-08 17:39:07 +08:00
|
|
|
}
|
|
|
|
|
2017-04-10 04:40:16 +08:00
|
|
|
authPlain(plain, next) {
|
|
|
|
if (!/^[a-zA-Z0-9+\/]+=+?$/.test(plain)) {
|
|
|
|
this.send('-ERR malformed command');
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let credentials = Buffer.from(plain, 'base64').toString().split('\x00');
|
|
|
|
if (credentials.length !== 3) {
|
|
|
|
this.send('-ERR malformed command');
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let username = credentials[1] || credentials[0] || '';
|
|
|
|
let password = credentials[2] || '';
|
|
|
|
|
|
|
|
this._server.onAuth({
|
|
|
|
method: 'PLAIN',
|
|
|
|
username,
|
|
|
|
password
|
|
|
|
}, this.session, (err, response) => {
|
|
|
|
|
|
|
|
if (err) {
|
|
|
|
this._server.logger.info({
|
|
|
|
err,
|
|
|
|
tnx: 'autherror',
|
|
|
|
cid: this._id,
|
|
|
|
method: 'PLAIN',
|
|
|
|
user: username
|
|
|
|
}, 'Authentication error for %s using %s. %s', username, 'PLAIN', err.message);
|
|
|
|
return next(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!response.user) {
|
|
|
|
this._server.logger.info({
|
|
|
|
tnx: 'authfail',
|
|
|
|
cid: this._id,
|
|
|
|
method: 'PLAIN',
|
|
|
|
user: username
|
|
|
|
}, 'Authentication failed for %s using %s', username, 'PLAIN');
|
|
|
|
this.send('-ERR [AUTH] ' + (response.message || 'Username and password not accepted'));
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
this._server.logger.info({
|
|
|
|
tnx: 'auth',
|
|
|
|
cid: this._id,
|
|
|
|
method: 'PLAIN',
|
|
|
|
user: username
|
|
|
|
}, '%s authenticated using %s', username, 'PLAIN');
|
|
|
|
this.session.user = response.user;
|
|
|
|
|
|
|
|
this.openMailbox(err => {
|
|
|
|
if (err) {
|
|
|
|
return next(err);
|
|
|
|
}
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-04-08 17:39:07 +08:00
|
|
|
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();
|
|
|
|
});
|
|
|
|
}
|
2017-04-08 02:29:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = POP3Connection;
|