'use strict'; const config = require('config'); const log = require('npmlog'); const POP3Server = require('./lib/pop3-server'); const fs = require('fs'); const UserHandler = require('./lib/user-handler'); const MessageHandler = require('./lib/message-handler'); const ObjectID = require('mongodb').ObjectID; const db = require('./lib/db'); const MAX_MESSAGES = 250; let messageHandler; let userHandler; const serverOptions = { port: config.pop3.port, host: config.pop3.host, secure: config.pop3.secure, // 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); } }, onAuth(auth, session, callback) { userHandler.authenticate(auth.username, auth.password, { protocol: 'POP3', ip: session.remoteAddress }, (err, result) => { if (err) { return callback(err); } if (!result) { return callback(); } if (result.scope === 'master' && result.enabled2fa) { // master password not allowed if 2fa is enabled! return callback(); } callback(null, { user: { id: result.user, username: result.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')); } session.user.mailbox = mailbox._id; db.database.collection('messages').find({ mailbox: mailbox._id }).project({ uid: true, size: true, // required to decide if we need to update flags after RETR flags: true, seen: true }).sort([ ['uid', -1] ]).limit(config.pop3.maxMessages || MAX_MESSAGES).toArray((err, messages) => { if (err) { return callback(err); } return callback(null, { messages: messages. // showolder first reverse(). // compose message objects map(message => ({ id: message._id.toString(), uid: message.uid, size: message.size, flags: message.flags, seen: message.seen })), count: messages.length, size: messages.reduce((acc, message) => acc + message.size, 0) }); }); }); }, onFetchMessage(id, session, callback) { db.database.collection('messages').findOne({ _id: new ObjectID(id) }, { 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')); } callback(null, response.value); }); }, onUpdate(update, session, callback) { let handleSeen = next => { if (update.seen && update.seen.length) { return markAsSeen(session, update.seen, next); } next(null, 0); }; let handleDeleted = next => { if (update.deleted && update.deleted.length) { return trashMessages(session, update.deleted, next); } next(null, 0); }; handleSeen((err, seenCount) => { if (err) { return log.error('POP3', err); } 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); }); }); // return callback without waiting for the update result setImmediate(callback); } }; if (config.pop3.key) { serverOptions.key = fs.readFileSync(config.pop3.key); } if (config.pop3.cert) { serverOptions.cert = fs.readFileSync(config.pop3.cert); } const server = new POP3Server(serverOptions); // move messages to trash function trashMessages(session, messages, callback) { // find Trash folder db.database.collection('mailboxes').findOne({ user: session.user.id, specialUse: '\\Trash' }, (err, trashMailbox) => { if (err) { return callback(err); } if (!trashMailbox) { return callback(new Error('Trash mailbox not found for user')); } 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 }, (err, success, meta) => { if (err) { return callback(err); } callback(null, success && meta && meta.destinationUid && meta.destinationUid.length || 0); }); }); } function markAsSeen(session, messages, callback) { let ids = messages.map(message => new ObjectID(message.id)); return db.database.collection('mailboxes').findOneAndUpdate({ _id: session.user.mailbox }, { $inc: { modifyIndex: 1 } }, { returnOriginal: false }, (err, item) => { if (err) { return callback(err); } let mailboxData = item && item.value; if (!item) { return callback(new Error('Mailbox does not exist')); } db.database.collection('messages').updateMany({ mailbox: mailboxData._id, _id: { $in: ids }, modseq: { $lt: mailboxData.modifyIndex } }, { $set: { modseq: mailboxData.modifyIndex, seen: true }, $addToSet: { flags: '\\Seen' } }, { multi: true, w: 1 }, err => { if (err) { return callback(err); } messageHandler.notifier.addEntries(mailboxData, false, messages.map(message => { let result = { command: 'FETCH', uid: message.uid, flags: message.flags.concat('\\Seen'), message: new ObjectID(message.id), modseq: mailboxData.modifyIndex }; return result; }), () => { messageHandler.notifier.fire(mailboxData.user, mailboxData.path); callback(null, messages.length); }); }); }); } module.exports = done => { if (!config.pop3.enabled) { return setImmediate(() => done(null, false)); } let started = false; messageHandler = new MessageHandler(db.database); userHandler = new UserHandler(db.database, db.redis); server.on('error', err => { if (!started) { started = true; return done(err); } log.error('POP3', err); }); server.listen(config.pop3.port, config.pop3.host, () => { if (started) { return server.close(); } started = true; done(null, server); }); };