diff --git a/imap-core/lib/imap-command.js b/imap-core/lib/imap-command.js index 63ba9c8b..f39f04ae 100644 --- a/imap-core/lib/imap-command.js +++ b/imap-core/lib/imap-command.js @@ -1,7 +1,7 @@ 'use strict'; const imapHandler = require('./handler/imap-handler'); - +const errors = require('../../lib/errors.js'); const MAX_MESSAGE_SIZE = 1 * 1024 * 1024; const commands = new Map([ @@ -88,6 +88,11 @@ class IMAPCommand { if (command.literal) { // check if the literal size is in acceptable bounds if (isNaN(command.expecting) || isNaN(command.expecting) < 0 || command.expecting > Number.MAX_SAFE_INTEGER) { + errors.notify(new Error('Invalid literal size'), { + command: { + expecting: command.expecting + } + }); this.connection.send(this.tag + ' BAD Invalid literal size'); return callback(new Error('Literal too big')); } diff --git a/lib/errors.js b/lib/errors.js index dcea0790..8495de6a 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -16,3 +16,21 @@ module.exports.notify = (...args) => { console.error(...args); } }; + +module.exports.intercept = (...args) => { + if (bugsnag) { + return bugsnag.intercept(...args); + } + let cb; + if (args.length) { + cb = args[args.length - 1]; + if (typeof cb === 'function') { + args[args.length - 1] = function(...rArgs) { + if (rArgs.length > 1 && rArgs[0]) { + console.error(rArgs[0]); + } + return cb(...rArgs); + }; + } + } +}; diff --git a/lib/message-handler.js b/lib/message-handler.js index e65bd101..fdba6dd1 100644 --- a/lib/message-handler.js +++ b/lib/message-handler.js @@ -105,6 +105,7 @@ class MessageHandler { this.checkExistingMessage( mailboxData, { + id, hdate, msgid, flags @@ -331,31 +332,44 @@ class MessageHandler { }); } - checkExistingMessage(mailboxData, message, options, callback) { + checkExistingMessage(mailboxData, messageOpts, options, callback) { // if a similar message already exists then update existing one + + let queryOpts = {}; + if (options.skipExisting) { + // no need to load extra data when we only need to know the basics + queryOpts.fields = { + _id: true, + uid: true + }; + } + this.database.collection('messages').findOne({ mailbox: mailboxData._id, - hdate: message.hdate, - msgid: message.msgid, + hdate: messageOpts.hdate, + msgid: messageOpts.msgid, uid: { $gt: 0, $lt: mailboxData.uidNext } - }, (err, existing) => { + }, queryOpts, (err, messageData) => { if (err) { return callback(err); } - if (!existing) { + if (!messageData) { // nothing to do here, continue adding message return callback(); } + let existingId = messageData._id; + let existingUid = messageData.uid; + if (options.skipExisting) { // message already exists, just skip it return callback(null, true, { - uid: existing.uid, - id: existing._id, + uid: existingUid, + id: existingId, mailbox: mailboxData._id, status: 'skip' }); @@ -368,7 +382,7 @@ class MessageHandler { _id: mailboxData._id }, { $inc: { - // allocate bot UID and MODSEQ values so when journal is later sorted by + // allocate both UID and MODSEQ values so when journal is later sorted by // modseq then UIDs are always in ascending order uidNext: 1, modifyIndex: 1 @@ -388,76 +402,75 @@ class MessageHandler { } let mailboxData = item.value; - let uid = mailboxData.uidNext; - let modseq = mailboxData.modifyIndex + 1; + let newUid = mailboxData.uidNext; + let newModseq = mailboxData.modifyIndex + 1; - this.database.collection('messages').findOneAndUpdate({ - _id: existing._id, - // hash key - mailbox: mailboxData._id, - uid: existing.uid - }, { - $set: { - uid, - modseq, - flags: message.flags - } - }, { - returnOriginal: false - }, (err, item) => { + // UID is immutable, so if we want to change it, we need to copy the message + + messageData._id = messageOpts.id; + messageData.uid = newUid; + messageOpts.modseq = newModseq; + messageData.flags = messageOpts.flags; + + this.database.collection('messages').insertOne(messageData, err => { if (err) { return callback(err); } - - if (!item || !item.value) { - // message was not found for whatever reason - return callback(); - } - - let updated = item.value; - - if (options.session && options.session.selected && options.session.selected.mailbox === mailboxData.path) { - options.session.writeStream.write(options.session.formatResponse('EXPUNGE', existing.uid)); - } - - if (options.session && options.session.selected && options.session.selected.mailbox === mailboxData.path) { - options.session.writeStream.write(options.session.formatResponse('EXISTS', updated.uid)); - } - this.notifier.addEntries( - mailboxData, - false, - { - command: 'EXPUNGE', - ignore: options.session && options.session.id, - uid: existing.uid, - message: existing._id, - unseen: existing.unseen - }, - () => { - this.notifier.addEntries( - mailboxData, - false, - { - command: 'EXISTS', - uid: updated.uid, - ignore: options.session && options.session.id, - message: updated._id, - modseq: updated.modseq, - unseen: updated.unseen - }, - () => { - this.notifier.fire(mailboxData.user, mailboxData.path); - return callback(null, true, { - uidValidity: mailboxData.uidValidity, - uid, - id: existing._id, - mailbox: mailboxData._id, - status: 'update' - }); - } - ); + this.database.collection('messages').deleteOne({ + _id: existingId, + // hash key + mailbox: mailboxData._id, + uid: existingUid + }, err => { + if (err) { + // TODO: how to resolve this? we might end up with two copies of the same message :S + return callback(err); } - ); + + if (options.session && options.session.selected && options.session.selected.mailbox === mailboxData.path) { + options.session.writeStream.write(options.session.formatResponse('EXPUNGE', existingUid)); + } + + if (options.session && options.session.selected && options.session.selected.mailbox === mailboxData.path) { + options.session.writeStream.write(options.session.formatResponse('EXISTS', messageData.uid)); + } + + this.notifier.addEntries( + mailboxData, + false, + { + command: 'EXPUNGE', + ignore: options.session && options.session.id, + uid: existingUid, + message: existingId, + unseen: messageData.unseen + }, + () => { + this.notifier.addEntries( + mailboxData, + false, + { + command: 'EXISTS', + uid: messageData.uid, + ignore: options.session && options.session.id, + message: messageData._id, + modseq: messageData.modseq, + unseen: messageData.unseen + }, + () => { + this.notifier.fire(mailboxData.user, mailboxData.path); + return callback(null, true, { + uidValidity: mailboxData.uidValidity, + uid: newUid, + id: existingId, + mailbox: mailboxData._id, + status: 'update' + }); + } + ); + } + ); + }); }); }); }); diff --git a/lmtp.js b/lmtp.js index 2f22e3f9..d31c70c3 100644 --- a/lmtp.js +++ b/lmtp.js @@ -384,7 +384,7 @@ const serverOptions = { } }); - let messageOptions = { + let messageOpts = { user: userData._id, [mailboxQueryKey]: mailboxQueryValue, @@ -404,17 +404,14 @@ const serverOptions = { messageHandler.encryptMessage(userData.encryptMessages ? userData.pubKey : false, raw, (err, encrypted) => { if (!err && encrypted) { - messageOptions.prepared = messageHandler.prepareMessage({ + messageOpts.prepared = messageHandler.prepareMessage({ raw: encrypted, indexedHeaders: spamHeaderKeys }); - messageOptions.maildata = messageHandler.indexer.getMaildata( - messageOptions.prepared.id, - messageOptions.prepared.mimeTree - ); + messageOpts.maildata = messageHandler.indexer.getMaildata(messageOpts.prepared.id, messageOpts.prepared.mimeTree); } - messageHandler.add(messageOptions, (err, inserted, info) => { + messageHandler.add(messageOpts, (err, inserted, info) => { // remove Delivered-To chunks.shift(); chunklen -= header.length;