wildduck/lib/mailbox-handler.js

415 lines
16 KiB
JavaScript
Raw Normal View History

2017-07-21 02:33:41 +08:00
'use strict';
const ObjectId = require('mongodb').ObjectId;
2017-07-21 02:33:41 +08:00
const ImapNotifier = require('./imap-notifier');
2020-10-09 16:08:33 +08:00
const { publish, MAILBOX_CREATED, MAILBOX_RENAMED, MAILBOX_DELETED } = require('./events');
const { SettingsHandler } = require('./settings-handler');
2017-07-21 02:33:41 +08:00
class MailboxHandler {
constructor(options) {
this.database = options.database;
this.users = options.users || options.database;
this.redis = options.redis;
2018-10-18 15:37:32 +08:00
this.loggelf = options.loggelf || (() => false);
2017-07-21 02:33:41 +08:00
this.notifier =
options.notifier ||
new ImapNotifier({
database: options.database,
redis: this.redis,
pushOnly: true
});
this.settingsHandler = new SettingsHandler({ db: this.database });
2017-07-21 02:33:41 +08:00
}
create(user, path, opts, callback) {
this.createAsync(user, path, opts)
.then(mailboxData => callback(null, ...[mailboxData.status, mailboxData.id]))
.catch(err => callback(err));
}
async createAsync(user, path, opts) {
const userData = await this.users.collection('users').findOne({ _id: user }, { projection: { retention: true } });
if (!userData) {
const err = new Error('This user does not exist');
err.code = 'UserNotFound';
err.responseCode = 404;
throw err;
}
let mailboxData = await this.database.collection('mailboxes').findOne({ user, path });
if (mailboxData) {
const err = new Error('Mailbox creation failed with code MailboxAlreadyExists');
err.code = 'ALREADYEXISTS';
err.responseCode = 400;
throw err;
}
const mailboxCountForUser = await this.database.collection('mailboxes').countDocuments({ user });
if (mailboxCountForUser > (await this.settingsHandler.get('const:max:mailboxes'))) {
const err = new Error('Mailbox creation failed with code ReachedMailboxCountLimit. Max mailboxes count reached.');
err.code = 'CANNOT';
err.responseCode = 400;
throw err;
}
mailboxData = {
_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)) {
mailboxData[key] = opts[key];
}
});
const r = this.database.collection('mailboxes').insertOne(mailboxData, { writeConcern: 'majority' });
try {
await publish(this.redis, {
ev: MAILBOX_CREATED,
user,
mailbox: r.insertedId,
path: mailboxData.path
});
} catch {
// ignore
}
await this.notifier.addEntries(
mailboxData,
{
command: 'CREATE',
mailbox: r.insertedId,
path
},
() => {
this.notifier.fire(user);
return;
}
);
return {
status: true,
id: mailboxData._id
};
2017-07-21 02:33:41 +08:00
}
rename(user, mailbox, newname, opts, callback) {
this.database.collection('mailboxes').findOne(
{
_id: mailbox,
user
},
(err, mailboxData) => {
2017-07-21 02:33:41 +08:00
if (err) {
return callback(err);
}
if (!mailboxData) {
const err = new Error('Mailbox update failed with code NoSuchMailbox');
err.code = 'NONEXISTENT';
err.responseCode = 404;
return callback(err, 'NONEXISTENT');
2017-07-21 02:33:41 +08:00
}
2020-07-02 18:08:16 +08:00
if (mailboxData.path === 'INBOX' || mailboxData.hidden) {
const err = new Error('Mailbox update failed with code DisallowedMailboxMethod');
err.code = 'CANNOT';
err.responseCode = 400;
return callback(err, 'CANNOT');
}
this.database.collection('mailboxes').findOne(
{
user: mailboxData.user,
path: newname
},
(err, existing) => {
if (err) {
return callback(err);
}
if (existing) {
const err = new Error('Mailbox rename failed with code MailboxAlreadyExists');
err.code = 'ALREADYEXISTS';
err.responseCode = 400;
return callback(err, 'ALREADYEXISTS');
}
2017-07-21 02:33:41 +08:00
let $set = { path: newname };
2017-07-21 02:33:41 +08:00
Object.keys(opts || {}).forEach(key => {
if (!['_id', 'user', 'path'].includes(key)) {
$set[key] = opts[key];
}
});
2017-07-21 02:33:41 +08:00
this.database.collection('mailboxes').findOneAndUpdate(
{
_id: mailbox
},
{
$set
},
{},
(err, item) => {
if (err) {
return callback(err);
}
2017-07-21 02:33:41 +08:00
if (!item || !item.value) {
// was not able to acquire a lock
const err = new Error('Mailbox update failed with code NoSuchMailbox');
err.code = 'NONEXISTENT';
err.responseCode = 404;
return callback(err, 'NONEXISTENT');
}
2020-10-09 16:08:33 +08:00
publish(this.redis, {
ev: MAILBOX_RENAMED,
user,
mailbox,
previous: mailboxData.path,
current: newname
}).catch(() => false);
this.notifier.addEntries(
mailboxData,
{
command: 'RENAME',
path: newname
},
() => {
this.notifier.fire(mailboxData.user);
2018-11-28 16:58:45 +08:00
return callback(null, true, mailbox);
}
);
}
);
2017-07-21 02:33:41 +08:00
}
);
}
);
2017-07-21 02:33:41 +08:00
}
2017-11-17 19:37:53 +08:00
/**
* Deletes a mailbox. Does not immediatelly release quota as the messages get deleted after a while
*/
2017-07-21 02:33:41 +08:00
del(user, mailbox, callback) {
this.database.collection('mailboxes').findOne(
{
_id: mailbox,
user
},
(err, mailboxData) => {
2017-07-21 02:33:41 +08:00
if (err) {
return callback(err);
}
if (!mailboxData) {
const err = new Error('Mailbox deletion failed with code NoSuchMailbox');
err.code = 'NONEXISTENT';
err.responseCode = 404;
return callback(err, 'NONEXISTENT');
}
2020-07-02 18:08:16 +08:00
if (mailboxData.specialUse || mailboxData.path === 'INBOX' || mailboxData.hidden) {
const err = new Error('Mailbox deletion failed with code DisallowedMailboxMethod');
err.code = 'CANNOT';
err.responseCode = 400;
return callback(err, 'CANNOT');
}
2017-07-21 02:33:41 +08:00
this.database.collection('mailboxes').deleteOne(
2017-07-21 02:33:41 +08:00
{
_id: mailbox
2017-07-21 02:33:41 +08:00
},
2021-02-26 20:00:13 +08:00
{ writeConcern: 'majority' },
2020-10-09 16:08:33 +08:00
(err, r) => {
if (err) {
return callback(err);
}
2017-11-17 19:37:53 +08:00
2020-10-09 16:08:33 +08:00
if (r.deletedCount) {
publish(this.redis, {
ev: MAILBOX_DELETED,
2019-03-20 21:00:57 +08:00
user,
2020-10-09 16:08:33 +08:00
mailbox,
path: mailboxData.path
}).catch(() => false);
}
let deleteFilters = async () => {
try {
let filters = await this.database
.collection('filters')
.find({
2019-03-20 21:00:57 +08:00
user,
2020-10-09 16:08:33 +08:00
'action.mailbox': mailbox
})
.toArray();
if (!filters) {
return;
}
for (let filterData of filters) {
// delete one by one for logging
try {
let r = await this.database.collection('filters').deleteOne({
_id: filterData._id
});
if (r && r.deletedCount) {
await publish(this.redis, {
ev: `filter.deleted`,
user,
filter: filterData._id
});
}
} catch (err) {
this.loggelf({
user,
mailbox,
action: 'delete_filter',
filter: filterData._id,
error: err.message
});
}
2019-03-20 21:00:57 +08:00
}
2020-10-09 16:08:33 +08:00
} catch (err) {
this.loggelf({
user,
mailbox,
action: 'delete_filter',
error: err.message
});
}
};
2019-03-20 21:00:57 +08:00
2020-10-09 16:08:33 +08:00
deleteFilters()
.then(() => {
// send information about deleted mailbox straight to connected clients
2019-03-20 21:00:57 +08:00
this.notifier.fire(mailboxData.user, {
command: 'DROP',
mailbox
});
this.notifier.addEntries(
mailboxData,
{
2019-03-20 21:00:57 +08:00
command: 'DELETE',
mailbox
},
2019-03-20 21:00:57 +08:00
() => {
this.database.collection('messages').updateMany(
{
2019-05-08 20:04:16 +08:00
mailbox
2019-03-20 21:00:57 +08:00
},
{
$set: {
exp: true,
// make sure the messages are in top of the expire queue
rdate: Date.now() - 24 * 3600 * 1000
}
},
{
multi: true,
2021-02-26 20:00:13 +08:00
writeConcern: 1
2019-03-20 21:00:57 +08:00
},
err => {
if (err) {
return callback(err);
}
2019-03-20 21:00:57 +08:00
let done = () => {
this.notifier.fire(mailboxData.user);
callback(null, true, mailbox);
};
2019-03-20 21:00:57 +08:00
return done();
}
);
}
);
2020-10-09 16:08:33 +08:00
})
.catch(() => false /* should not happen */);
2017-07-21 02:33:41 +08:00
}
);
}
);
2017-07-21 02:33:41 +08:00
}
update(user, mailbox, updates, callback) {
if (!updates) {
return callback(null, false);
}
this.database.collection('mailboxes').findOne(
{
2017-07-21 02:33:41 +08:00
_id: mailbox
},
(err, mailboxData) => {
2017-07-21 02:33:41 +08:00
if (err) {
return callback(err);
}
if (!mailboxData) {
const err = new Error('Mailbox update failed with code NoSuchMailbox');
err.code = 'NONEXISTENT';
err.responseCode = 404;
return callback(err, 'NONEXISTENT');
2017-07-21 02:33:41 +08:00
}
2020-07-07 16:06:00 +08:00
if (updates.path && updates.path !== mailboxData.path) {
return this.rename(user, mailbox, updates.path, updates, callback);
}
2017-07-21 02:33:41 +08:00
let $set = {};
2018-06-26 19:46:03 +08:00
let hasChanges = false;
Object.keys(updates || {}).forEach(key => {
if (!['_id', 'user', 'path'].includes(key)) {
$set[key] = updates[key];
2018-06-26 19:46:03 +08:00
hasChanges = true;
}
});
2018-06-26 19:46:03 +08:00
if (!hasChanges) {
return callback(null, true);
}
this.database.collection('mailboxes').findOneAndUpdate(
{
_id: mailbox
},
{
$set
},
{},
(err, item) => {
if (err) {
return callback(err);
}
if (!item || !item.value) {
const err = new Error('Mailbox update failed with code NoSuchMailbox');
err.code = 'NONEXISTENT';
err.responseCode = 404;
return callback(err, 'NONEXISTENT');
}
return callback(null, true);
}
);
}
);
2017-07-21 02:33:41 +08:00
}
}
module.exports = MailboxHandler;