wildduck/lib/message-handler.js

301 lines
11 KiB
JavaScript
Raw Normal View History

'use strict';
const config = require('config');
const uuidV1 = require('uuid/v1');
const ObjectID = require('mongodb').ObjectID;
const RedFour = require('redfour');
const Indexer = require('../imap-core/lib/indexer/indexer');
const ImapNotifier = require('./imap-notifier');
class MessageHandler {
constructor(database) {
this.database = database;
this.indexer = new Indexer({
database
});
this.notifier = new ImapNotifier({
database,
pushOnly: true
});
this.redlock = new RedFour({
redis: config.redis,
namespace: 'wildduck'
});
}
getMailbox(options, callback) {
let query = {};
if (options.mailbox) {
if (typeof options.mailbox === 'object' && options.mailbox._id) {
return setImmediate(null, options.mailbox);
}
query._id = options.mailbox;
} else {
query.user = options.user;
query.path = options.path;
}
this.database.collection('mailboxes').findOne(query, (err, mailbox) => {
if (err) {
return callback(err);
}
if (!mailbox) {
let err = new Error('Mailbox is missing');
err.imapResponse = 'TRYCREATE';
return callback(err);
}
callback(null, mailbox);
});
}
add(options, callback) {
let id = new ObjectID();
let mimeTree = this.indexer.parseMimeTree(options.raw);
let size = this.indexer.getSize(mimeTree);
let bodystructure = this.indexer.getBodyStructure(mimeTree);
let envelope = this.indexer.getEnvelope(mimeTree);
let messageId = envelope[9] || ('<' + uuidV1() + '@wildduck.email>');
this.getMailbox(options, (err, mailbox) => {
if (err) {
return callback(err);
}
this.indexer.storeAttachments(id, mimeTree, 50 * 1024, err => {
if (err) {
return callback(err);
}
// Another server might be waiting for the lock like this.
this.redlock.waitAcquireLock(mailbox._id.toString(), 30 * 1000, 10 * 1000, (err, lock) => {
if (err) {
return callback(err);
}
if (!lock || !lock.success) {
// did not get a insert lock in 10 seconds
return callback(new Error('The user you are trying to contact is receiving mail at a rate that prevents additional messages from being delivered. Please resend your message at a later time'));
}
2017-03-27 04:58:05 +08:00
this.database.collection('users').findOneAndUpdate({
_id: mailbox.user
}, {
$inc: {
2017-03-27 04:58:05 +08:00
storageUsed: size,
messages: 1
}
2017-03-27 04:58:05 +08:00
}, err => {
if (err) {
this.redlock.releaseLock(lock, () => false);
return callback(err);
}
2017-03-27 04:58:05 +08:00
let rollback = err => {
this.database.collection('users').findOneAndUpdate({
_id: mailbox.user
}, {
$inc: {
storageUsed: -size,
messages: -1
}
}, () => {
this.redlock.releaseLock(lock, () => callback(err));
});
};
2017-03-27 04:58:05 +08:00
// acquire new UID+MODSEQ
this.database.collection('mailboxes').findOneAndUpdate({
_id: mailbox._id
}, {
$inc: {
// allocate bot UID and MODSEQ values so when journal is later sorted by
// modseq then UIDs are always in ascending order
uidNext: 1,
modifyIndex: 1,
storageUsed: size,
messages: 1
}
}, (err, item) => {
if (err) {
2017-03-27 04:58:05 +08:00
return rollback(err);
}
if (!item || !item.value) {
// was not able to acquire a lock
let err = new Error('Mailbox is missing');
err.imapResponse = 'TRYCREATE';
return rollback(err);
}
2017-03-27 04:58:05 +08:00
let mailbox = item.value;
2017-03-27 04:58:05 +08:00
let internaldate = options.date && new Date(options.date) || new Date();
let headerdate = mimeTree.parsedHeader.date && new Date(mimeTree.parsedHeader.date) || false;
if (!headerdate || headerdate.toString() === 'Invalid Date') {
headerdate = internaldate;
}
2017-03-27 04:58:05 +08:00
let message = {
_id: id,
mailbox: mailbox._id,
uid: mailbox.uidNext,
internaldate,
headerdate,
flags: [].concat(options.flags || []),
size,
meta: options.meta || {},
modseq: mailbox.modifyIndex + 1,
mimeTree,
envelope,
bodystructure,
messageId
};
this.database.collection('messages').insertOne(message, err => {
if (err) {
return this.database.collection('mailboxes').findOneAndUpdate({
_id: mailbox._id
}, {
$inc: {
storageUsed: -size,
messages: -1
}
}, () => rollback(err));
}
let uidValidity = mailbox.uidValidity;
let uid = message.uid;
this.notifier.addEntries(mailbox, false, {
command: 'EXISTS',
uid: message.uid,
message: message._id,
modseq: message.modseq
}, () => {
this.redlock.releaseLock(lock, () => {
this.notifier.fire(mailbox.user, mailbox.path);
return callback(null, true, {
uidValidity,
uid
});
});
});
});
});
});
});
});
});
}
updateQuota(mailbox, inc, callback) {
inc = inc || {};
if (!inc.messages) {
return callback();
}
this.database.collection('mailboxes').findOneAndUpdate({
_id: mailbox._id
}, {
$inc: {
storageUsed: Number(inc.storageUsed) || 0,
messages: Number(inc.messages) || 0
}
}, () => {
this.database.collection('users').findOneAndUpdate({
_id: mailbox.user
}, {
$inc: {
storageUsed: Number(inc.storageUsed) || 0,
messages: Number(inc.messages) || 0
}
}, callback);
});
}
del(messageId, callback) {
this.database.collection('messages').findOne({
_id: typeof messageId === 'string' ? new ObjectID(messageId) : messageId
}, (err, message) => {
if (err) {
return callback(err);
}
if (!message) {
return callback(new Error('Message does not exist'));
}
this.database.collection('mailboxes').findOne({
_id: message.mailbox
}, (err, mailbox) => {
if (err) {
return callback(err);
}
if (!mailbox) {
return callback(new Error('Mailbox does not exist'));
}
this.database.collection('messages').deleteOne({
_id: message._id
}, err => {
if (err) {
return callback(err);
}
this.updateQuota(mailbox, {
storageUsed: -message.size,
messages: -1
}, () => {
// remove link to message from attachments (if any exist)
this.database.collection('attachments.files').updateMany({
'metadata.messages': message._id
}, {
$pull: {
'metadata.messages': message._id
}
}, {
multi: true,
w: 1
}, err => {
if (err) {
// ignore as we don't really care if we have orphans or not
}
this.notifier.addEntries(mailbox, false, {
command: 'EXPUNGE',
uid: message.uid,
message: message._id
}, () => {
this.notifier.fire(mailbox.user, mailbox.path);
// delete all attachments that do not have any active links to message objects
this.database.collection('attachments.files').deleteMany({
'metadata.messages': {
$size: 0
}
}, err => {
if (err) {
// ignore as we don't really care if we have orphans or not
}
return callback(null, true);
});
});
});
});
});
});
});
}
}
module.exports = MessageHandler;