wildduck/pop3.js

503 lines
17 KiB
JavaScript
Raw Normal View History

2017-04-08 02:29:14 +08:00
'use strict';
2017-07-16 19:37:33 +08:00
const config = require('wild-config');
2017-04-08 02:29:14 +08:00
const log = require('npmlog');
const POP3Server = require('./lib/pop3/server');
2017-04-22 03:20:11 +08:00
const UserHandler = require('./lib/user-handler');
const MessageHandler = require('./lib/message-handler');
2017-11-06 22:23:27 +08:00
const packageData = require('./package.json');
const ObjectId = require('mongodb').ObjectId;
2017-04-08 03:38:52 +08:00
const db = require('./lib/db');
const certs = require('./lib/certs');
2017-11-13 21:27:37 +08:00
const LimitedFetch = require('./lib/limited-fetch');
2022-07-05 16:57:57 +08:00
const tools = require('./lib/tools');
2018-10-18 15:37:32 +08:00
const Gelf = require('gelf');
const os = require('os');
2017-04-08 02:29:14 +08:00
2017-04-09 06:18:31 +08:00
const MAX_MESSAGES = 250;
let messageHandler;
2017-04-22 03:20:11 +08:00
let userHandler;
2018-10-18 15:37:32 +08:00
let loggelf;
2017-04-08 02:29:14 +08:00
const serverOptions = {
port: config.pop3.port,
host: config.pop3.host,
2017-11-06 22:23:27 +08:00
2017-04-08 02:29:14 +08:00
secure: config.pop3.secure,
2017-12-01 16:02:40 +08:00
secured: config.pop3.secured,
2017-11-06 22:23:27 +08:00
disableSTARTTLS: config.pop3.disableSTARTTLS,
ignoreSTARTTLS: config.pop3.ignoreSTARTTLS,
2017-08-23 21:29:35 +08:00
disableVersionString: !!config.pop3.disableVersionString,
2017-04-08 02:29:14 +08:00
2017-10-12 03:57:39 +08:00
useProxy: !!config.imap.useProxy,
ignoredHosts: config.pop3.ignoredHosts,
2017-10-12 03:57:39 +08:00
2017-11-06 22:23:27 +08:00
id: {
2018-01-02 21:04:01 +08:00
name: config.pop3.name || 'WildDuck POP3 Server',
2017-11-06 22:23:27 +08:00
version: config.pop3.version || packageData.version
},
SNICallback(servername, cb) {
certs
2021-10-08 22:30:15 +08:00
.getContextForServername(
servername,
serverOptions,
{
source: 'pop3'
},
{
loggelf: message => loggelf(message)
}
)
.then(context => cb(null, context))
.catch(err => cb(err));
},
2017-04-08 02:29:14 +08:00
// log to console
logger: {
info(...args) {
args.shift();
log.info('POP3', ...args);
},
debug(...args) {
args.shift();
log.silly('POP3', ...args);
},
error(...args) {
args.shift();
log.error('POP3', ...args);
}
2017-04-08 03:38:52 +08:00
},
onAuth(auth, session, callback) {
2017-06-03 14:51:58 +08:00
userHandler.authenticate(
auth.username,
auth.password,
2017-07-24 21:44:08 +08:00
'pop3',
2017-06-03 14:51:58 +08:00
{
protocol: 'POP3',
2017-10-30 19:41:53 +08:00
sess: session.id,
2017-06-03 14:51:58 +08:00
ip: session.remoteAddress
},
(err, result) => {
if (err) {
return callback(err);
}
2017-11-13 21:27:37 +08:00
2017-06-03 14:51:58 +08:00
if (!result) {
return callback();
}
2017-04-08 03:38:52 +08:00
2017-07-24 21:44:08 +08:00
if (result.scope === 'master' && result.require2fa) {
2017-06-03 14:51:58 +08:00
// master password not allowed if 2fa is enabled!
return callback();
2017-04-08 03:38:52 +08:00
}
2017-06-03 14:51:58 +08:00
callback(null, {
user: {
id: result.user,
username: result.username
}
});
}
);
},
onListMessages(session, callback) {
// only list messages in INBOX
2017-12-01 16:02:40 +08:00
db.database.collection('mailboxes').findOne(
{
user: session.user.id,
path: 'INBOX'
},
(err, mailbox) => {
if (err) {
return callback(err);
}
2017-12-01 16:02:40 +08:00
if (!mailbox) {
return callback(new Error('Mailbox not found for user'));
}
2017-06-03 14:51:58 +08:00
2017-12-01 16:02:40 +08:00
session.user.mailbox = mailbox._id;
2020-07-09 01:50:51 +08:00
db.redis
.multi()
// "new" limit store
.hget(`pxm:${session.user.id}`, mailbox._id.toString())
// fallback store
.hget(`pop3uid`, mailbox._id.toString())
.exec((err, res) => {
let lastIndex = res && ((res[0] && res[0][1]) || (res[1] && res[1][1]));
let query = {
mailbox: mailbox._id
};
if (!err && lastIndex && !isNaN(lastIndex)) {
query.uid = { $gte: Number(lastIndex) };
}
userHandler.userCache.get(session.user.id, 'pop3MaxMessages', config.pop3.maxMessages, (err, maxMessages) => {
2019-03-13 21:39:13 +08:00
if (err) {
return callback(err);
}
2020-07-09 01:50:51 +08:00
db.database
.collection('messages')
.find(query)
.project({
uid: true,
size: true,
mailbox: true,
// required to decide if we need to update flags after RETR
flags: true,
unseen: true
})
.sort({ uid: -1 })
.limit(maxMessages || MAX_MESSAGES)
.toArray((err, messages) => {
if (err) {
return callback(err);
}
let updateUIDIndex = done => {
// first is the newest, last the oldest
let oldestMessageData = messages && messages.length && messages[messages.length - 1];
if (!oldestMessageData || !oldestMessageData.uid) {
return done();
}
// try to update index, ignore result
db.redis
.multi()
// update limit store
.hset(`pxm:${session.user.id}`, mailbox._id.toString(), oldestMessageData.uid)
// delete fallback store as it is no longer needed
.hdel(`pop3uid`, mailbox._id.toString())
.exec(done);
};
2021-02-26 22:43:29 +08:00
updateUIDIndex(() =>
callback(null, {
2020-07-09 01:50:51 +08:00
messages: messages
// show older first
.reverse()
// compose message objects
.map(message => ({
id: message._id.toString(),
uid: message.uid,
mailbox: message.mailbox,
size: message.size,
flags: message.flags,
seen: !message.unseen
})),
count: messages.length,
size: messages.reduce((acc, message) => acc + message.size, 0)
2021-02-26 22:43:29 +08:00
})
);
2019-03-13 21:39:13 +08:00
});
2017-12-01 16:02:40 +08:00
});
2020-07-09 01:50:51 +08:00
});
2017-12-01 16:02:40 +08:00
}
);
},
onFetchMessage(message, session, callback) {
2022-07-05 16:57:57 +08:00
userHandler.userCache.get(session.user.id, 'pop3MaxDownload', { setting: 'const:max:pop3:download' }, (err, limit) => {
2017-12-08 20:29:00 +08:00
if (err) {
return callback(err);
}
2017-12-08 20:29:00 +08:00
messageHandler.counters.ttlcounter('pdw:' + session.user.id, 0, limit, false, (err, res) => {
if (err) {
return callback(err);
}
if (!res.success) {
2022-07-05 16:57:57 +08:00
let err = new Error('Download was rate limited');
err.response = 'NO';
err.code = 'DownloadRateLimited';
err.ttl = res.ttl;
err.responseMessage = `Download was rate limited. Try again in ${tools.roundTime(res.ttl)}.`;
return callback(err);
}
2022-07-05 16:57:57 +08:00
db.database.collection('messages').findOne(
{
_id: new ObjectId(message.id),
// shard key
mailbox: message.mailbox,
uid: message.uid
},
{
mimeTree: true,
size: true
},
(err, message) => {
if (err) {
return callback(err);
}
if (!message) {
return callback(new Error('Message does not exist or is already deleted'));
}
let response = messageHandler.indexer.rebuild(message.mimeTree);
if (!response || response.type !== 'stream' || !response.value) {
return callback(new Error('Can not fetch message'));
}
2017-11-13 21:27:37 +08:00
let limiter = new LimitedFetch({
key: 'pdw:' + session.user.id,
ttlcounter: messageHandler.counters.ttlcounter,
maxBytes: limit
});
2017-11-13 21:27:37 +08:00
response.value.pipe(limiter);
response.value.once('error', err => limiter.emit('error', err));
callback(null, limiter);
}
);
});
});
2017-04-09 06:18:31 +08:00
},
onUpdate(update, session, callback) {
let handleSeen = next => {
if (update.seen && update.seen.length) {
2017-04-09 17:33:10 +08:00
return markAsSeen(session, update.seen, next);
2017-04-09 06:18:31 +08:00
}
2017-04-09 17:53:12 +08:00
next(null, 0);
2017-04-09 06:18:31 +08:00
};
2017-04-09 17:33:10 +08:00
let handleDeleted = next => {
if (update.deleted && update.deleted.length) {
return trashMessages(session, update.deleted, next);
}
2017-04-09 17:53:12 +08:00
next(null, 0);
2017-04-09 17:33:10 +08:00
};
handleSeen((err, seenCount) => {
2017-04-09 06:18:31 +08:00
if (err) {
return log.error('POP3', err);
}
2017-04-09 17:33:10 +08:00
handleDeleted((err, deleteCount) => {
if (err) {
return log.error('POP3', err);
}
log.info('POP3', '[%s] Deleted %s messages, marked %s messages as seen', session.user.username, deleteCount, seenCount);
});
2017-04-09 06:18:31 +08:00
});
// return callback without waiting for the update result
setImmediate(callback);
}
2017-04-08 02:29:14 +08:00
};
certs.loadTLSOptions(serverOptions, 'pop3');
2017-04-08 02:29:14 +08:00
const server = new POP3Server(serverOptions);
certs.registerReload(server, 'pop3');
2017-04-09 17:33:10 +08:00
// move messages to trash
function trashMessages(session, messages, callback) {
// find Trash folder
2017-12-01 16:02:40 +08:00
db.database.collection('mailboxes').findOne(
{
user: session.user.id,
specialUse: '\\Trash'
},
(err, trashMailbox) => {
if (err) {
return callback(err);
}
2017-04-09 17:33:10 +08:00
2017-12-01 16:02:40 +08:00
if (!trashMailbox) {
return callback(new Error('Trash mailbox not found for user'));
}
2017-04-09 17:33:10 +08:00
2017-12-01 16:02:40 +08:00
messageHandler.move(
{
user: session.user.id,
// folder to move messages from
source: {
mailbox: session.user.mailbox
},
// folder to move messages to
destination: trashMailbox,
// list of UIDs to move
messages: messages.map(message => message.uid),
// add \Seen flags to deleted messages
markAsSeen: true
2017-06-03 14:51:58 +08:00
},
2017-12-01 16:02:40 +08:00
(err, success, meta) => {
if (err) {
return callback(err);
}
callback(null, (success && meta && meta.destinationUid && meta.destinationUid.length) || 0);
2017-06-03 14:51:58 +08:00
}
2017-12-01 16:02:40 +08:00
);
}
);
2017-04-09 17:33:10 +08:00
}
function markAsSeen(session, messages, callback) {
let ids = messages.map(message => new ObjectId(message.id));
2017-04-09 06:18:31 +08:00
2017-12-01 16:02:40 +08:00
return db.database.collection('mailboxes').findOneAndUpdate(
{
_id: session.user.mailbox
},
{
$inc: {
modifyIndex: 1
2017-04-09 06:18:31 +08:00
}
2017-12-01 16:02:40 +08:00
},
{
2021-06-15 15:47:18 +08:00
returnDocument: 'after'
2017-12-01 16:02:40 +08:00
},
(err, item) => {
2017-04-09 06:18:31 +08:00
if (err) {
return callback(err);
}
2017-12-01 16:02:40 +08:00
let mailboxData = item && item.value;
if (!item) {
2018-10-12 16:13:54 +08:00
let err = new Error('Selected mailbox does not exist');
2021-05-22 01:14:43 +08:00
err.responseCode = 404;
2018-10-12 16:13:54 +08:00
err.code = 'NoSuchMailbox';
return callback(err);
2017-12-01 16:02:40 +08:00
}
db.database.collection('messages').updateMany(
{
_id: {
$in: ids
},
user: session.user.id,
mailbox: mailboxData._id,
modseq: {
$lt: mailboxData.modifyIndex
}
},
{
$set: {
2017-07-20 21:10:36 +08:00
modseq: mailboxData.modifyIndex,
2017-12-01 16:02:40 +08:00
unseen: false
},
$addToSet: {
flags: '\\Seen'
}
},
{
multi: true,
2021-02-26 20:00:13 +08:00
writeConcern: 1
2017-12-01 16:02:40 +08:00
},
err => {
if (err) {
return callback(err);
}
messageHandler.notifier.addEntries(
mailboxData,
messages.map(message => {
let result = {
command: 'FETCH',
uid: message.uid,
flags: message.flags.concat('\\Seen'),
message: new ObjectId(message.id),
2017-12-01 16:02:40 +08:00
modseq: mailboxData.modifyIndex,
// Indicate that unseen values are changed. Not sure how much though
unseenChange: true
};
return result;
}),
() => {
messageHandler.notifier.fire(mailboxData.user);
2017-12-01 16:02:40 +08:00
callback(null, messages.length);
}
);
2017-06-03 14:51:58 +08:00
}
);
2017-12-01 16:02:40 +08:00
}
);
2017-04-09 06:18:31 +08:00
}
2017-04-08 02:29:14 +08:00
module.exports = done => {
if (!config.pop3.enabled) {
return setImmediate(() => done(null, false));
}
let started = false;
2018-10-18 15:37:32 +08:00
const component = config.log.gelf.component || 'wildduck';
const hostname = config.log.gelf.hostname || os.hostname();
const gelf =
config.log.gelf && config.log.gelf.enabled
? new Gelf(config.log.gelf.options)
: {
// placeholder
emit: (key, message) => log.info('Gelf', JSON.stringify(message))
2018-10-18 15:37:32 +08:00
};
loggelf = message => {
if (typeof message === 'string') {
message = {
short_message: message
};
}
message = message || {};
2018-10-18 16:53:14 +08:00
if (!message.short_message || message.short_message.indexOf(component.toUpperCase()) !== 0) {
message.short_message = component.toUpperCase() + ' ' + (message.short_message || '');
}
2018-10-18 15:37:32 +08:00
message.facility = component; // facility is deprecated but set by the driver if not provided
message.host = hostname;
message.timestamp = Date.now() / 1000;
message._component = component;
Object.keys(message).forEach(key => {
if (!message[key]) {
delete message[key];
}
});
gelf.emit('gelf.log', message);
};
2017-08-07 02:25:10 +08:00
messageHandler = new MessageHandler({
2018-12-03 19:35:00 +08:00
users: db.users,
2017-08-07 02:25:10 +08:00
database: db.database,
redis: db.redis,
2017-08-07 02:25:10 +08:00
gridfs: db.gridfs,
2018-10-18 15:37:32 +08:00
attachments: config.attachments,
loggelf: message => loggelf(message)
2017-08-07 02:25:10 +08:00
});
userHandler = new UserHandler({
database: db.database,
users: db.users,
2017-08-08 19:35:18 +08:00
redis: db.redis,
2018-10-18 15:37:32 +08:00
loggelf: message => loggelf(message)
2017-08-07 02:25:10 +08:00
});
2020-05-08 15:43:59 +08:00
server.loggelf = loggelf;
2017-04-08 02:29:14 +08:00
server.on('error', err => {
if (!started) {
started = true;
return done(err);
}
2017-10-08 04:32:15 +08:00
log.error('POP3', err.message);
2017-04-08 02:29:14 +08:00
});
server.listen(config.pop3.port, config.pop3.host, () => {
if (started) {
return server.close();
}
started = true;
done(null, server);
});
};