'use strict'; const ObjectID = require('mongodb').ObjectID; const ImapNotifier = require('./imap-notifier'); class MailboxHandler { constructor(options) { this.database = options.database; this.users = options.users || options.database; this.redis = options.redis; this.notifier = options.notifier || new ImapNotifier({ database: options.database, redis: this.redis, pushOnly: true }); } create(user, path, opts, callback) { this.database.collection('mailboxes').findOne({ user, path }, (err, mailbox) => { if (err) { return callback(err); } if (mailbox) { return callback(null, 'ALREADYEXISTS'); } this.users.collection('users').findOne({ _id: user }, { fields: { retention: true } }, (err, userData) => { if (err) { return callback(err); } if (!userData) { return callback(new Error('User not found')); } mailbox = { _id: new ObjectID(), user, path, uidValidity: Math.floor(Date.now() / 1000), uidNext: 1, modifyIndex: 0, subscribed: true, flags: [], retention: userData.retention }; Object.keys(opts || {}).forEach(key => { if (!['_id', 'user', 'path'].includes(key)) { mailbox[key] = opts[key]; } }); this.database.collection('mailboxes').insertOne(mailbox, (err, r) => { if (err) { if (err.code === 11000) { return callback(null, 'ALREADYEXISTS'); } return callback(err); } return this.notifier.addEntries( user, path, { command: 'CREATE', mailbox: r.insertedId, path }, () => { this.notifier.fire(user, path); return callback(null, true, mailbox._id); } ); }); }); }); } rename(user, mailbox, newname, opts, callback) { this.database.collection('mailboxes').findOne({ _id: mailbox, user }, (err, mailboxData) => { if (err) { return callback(err); } if (!mailboxData) { return callback(null, 'NONEXISTENT'); } if (mailboxData.path === 'INBOX') { return callback(null, 'CANNOT'); } this.database.collection('mailboxes').findOne({ user: mailboxData.user, path: newname }, (err, existing) => { if (err) { return callback(err); } if (existing) { return callback(null, 'ALREADYEXISTS'); } let $set = { path: newname }; Object.keys(opts || {}).forEach(key => { if (!['_id', 'user', 'path'].includes(key)) { $set[key] = opts[key]; } }); this.database.collection('mailboxes').findOneAndUpdate({ _id: mailbox }, { $set }, {}, (err, item) => { if (err) { return callback(err); } if (!item || !item.value) { // was not able to acquire a lock return callback(null, 'NONEXISTENT'); } this.notifier.addEntries( mailboxData, false, { command: 'RENAME', path: newname }, () => { this.notifier.fire(mailboxData.user, mailboxData.path); return callback(null, true); } ); }); }); }); } /** * Deletes a mailbox. Does not immediatelly release quota as the messages get deleted after a while */ del(user, mailbox, callback) { this.database.collection('mailboxes').findOne({ _id: mailbox, user }, (err, mailboxData) => { if (err) { return callback(err); } if (!mailboxData) { return callback(null, 'NONEXISTENT'); } if (mailboxData.specialUse || mailboxData.path === 'INBOX') { return callback(null, 'CANNOT'); } this.database.collection('mailboxes').deleteOne({ _id: mailbox }, err => { if (err) { return callback(err); } this.notifier.addEntries( mailboxData, false, { command: 'DROP', mailbox }, () => { this.database.collection('messages').updateMany({ mailbox, uid: { $gt: 0, $lt: mailboxData.uidNext + 100 } }, { $set: { exp: true, // make sure the messages are in top of the expire queue rdate: Date.now() - 24 * 3600 * 1000 } }, { multi: true, w: 1 }, err => { if (err) { return callback(err); } let done = () => { this.notifier.fire(mailboxData.user, mailboxData.path); callback(null, true); }; return done(); }); } ); }); }); } update(user, mailbox, updates, callback) { if (!updates) { return callback(null, false); } this.database.collection('mailboxes').findOne({ _id: mailbox }, (err, mailboxData) => { if (err) { return callback(err); } if (!mailboxData) { return callback(null, 'NONEXISTENT'); } if (updates.path !== mailboxData.path) { return this.rename(user, mailbox, updates.path, updates, callback); } let $set = {}; Object.keys(updates || {}).forEach(key => { if (!['_id', 'user', 'path'].includes(key)) { $set[key] = updates[key]; } }); this.database.collection('mailboxes').findOneAndUpdate({ _id: mailbox }, { $set }, {}, (err, item) => { if (err) { return callback(err); } if (!item || !item.value) { // was not able to acquire a lock return callback(null, 'NONEXISTENT'); } return callback(null, true); }); }); } } module.exports = MailboxHandler;