Copy similar message on APPEND, do not overwrite

This commit is contained in:
Andris Reinman 2017-10-02 11:48:22 +03:00
parent cc80fdb5e9
commit 7d1cff7818
4 changed files with 114 additions and 81 deletions

View file

@ -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'));
}

View file

@ -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);
};
}
}
};

View file

@ -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'
});
}
);
}
);
});
});
});
});

11
lmtp.js
View file

@ -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;