wildduck/lib/api/mailboxes.js
2022-11-24 12:26:10 +02:00

594 lines
19 KiB
JavaScript

'use strict';
const Joi = require('joi');
const ObjectId = require('mongodb').ObjectId;
const imapTools = require('../../imap-core/lib/imap-tools');
const tools = require('../tools');
const roles = require('../roles');
const util = require('util');
const { sessSchema, sessIPSchema, booleanSchema } = require('../schemas');
module.exports = (db, server, mailboxHandler) => {
const getMailboxCounter = util.promisify(tools.getMailboxCounter);
const updateMailbox = util.promisify(mailboxHandler.update.bind(mailboxHandler));
const deleteMailbox = util.promisify(mailboxHandler.del.bind(mailboxHandler));
const createMailbox = util.promisify((...args) => {
let callback = args.pop();
mailboxHandler.create(...args, (err, status, id) => {
if (err) {
return callback(err);
}
return callback(null, { status, id });
});
});
server.get(
'/users/:user/mailboxes',
tools.responseWrapper(async (req, res) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
user: Joi.string().hex().lowercase().length(24).required(),
specialUse: booleanSchema.default(false),
showHidden: booleanSchema.default(false),
counters: booleanSchema.default(false),
sizes: booleanSchema.default(false),
sess: sessSchema,
ip: sessIPSchema
});
const result = schema.validate(req.params, {
abortEarly: false,
convert: true
});
if (result.error) {
res.status(400);
return res.json({
error: result.error.message,
code: 'InputValidationError',
details: tools.validationErrors(result)
});
}
// permissions check
if (req.user && req.user === result.value.user) {
req.validate(roles.can(req.role).readOwn('mailboxes'));
} else {
req.validate(roles.can(req.role).readAny('mailboxes'));
}
let user = new ObjectId(result.value.user);
let counters = result.value.counters;
let sizes = result.value.sizes;
let sizeValues = false;
let userData;
try {
userData = await db.users.collection('users').findOne(
{
_id: user
},
{
projection: {
address: true
}
}
);
} catch (err) {
res.status(500);
return res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
}
if (!userData) {
res.status(404);
return res.json({
error: 'This user does not exist',
code: 'UserNotFound'
});
}
if (sizes) {
try {
sizeValues = await db.database
.collection('messages')
.aggregate([
{
$match: {
user
}
},
{
$project: {
mailbox: '$mailbox',
size: '$size'
}
},
{
$group: {
_id: '$mailbox',
mailboxSize: {
$sum: '$size'
}
}
}
])
.toArray();
} catch (err) {
// ignore
}
}
let mailboxes;
try {
mailboxes = await db.database
.collection('mailboxes')
.find({
user
})
.toArray();
} catch (err) {
res.status(500);
return res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
}
if (!mailboxes) {
mailboxes = [];
}
if (result.value.specialUse) {
mailboxes = mailboxes.filter(mailboxData => mailboxData.path === 'INBOX' || mailboxData.specialUse);
}
if (!result.value.showHidden) {
mailboxes = mailboxes.filter(mailboxData => !mailboxData.hidden);
}
mailboxes = mailboxes
.map(mailboxData => mailboxData)
.sort((a, b) => {
if (a.path === 'INBOX') {
return -1;
}
if (b.path === 'INBOX') {
return 1;
}
if (a.path.indexOf('INBOX/') === 0 && b.path.indexOf('INBOX/') !== 0) {
return -1;
}
if (a.path.indexOf('INBOX/') !== 0 && b.path.indexOf('INBOX/') === 0) {
return 1;
}
if (a.subscribed !== b.subscribed) {
return (a.subscribed ? 0 : 1) - (b.subscribed ? 0 : 1);
}
return a.path.localeCompare(b.path);
});
let responses = [];
let counterOps = [];
for (let mailboxData of mailboxes) {
let path = mailboxData.path.split('/');
let name = path.pop();
let response = {
id: mailboxData._id.toString(),
name,
path: mailboxData.path,
specialUse: mailboxData.specialUse,
modifyIndex: mailboxData.modifyIndex,
subscribed: mailboxData.subscribed,
hidden: !mailboxData.hidden
};
if (mailboxData.retention) {
response.retention = mailboxData.retention;
}
if (sizeValues) {
for (let sizeValue of sizeValues) {
if (mailboxData._id.equals(sizeValue._id)) {
response.size = sizeValue.mailboxSize;
break;
}
}
}
if (!counters) {
responses.push(response);
continue;
}
let total, unseen;
counterOps.push(
(async () => {
try {
total = await getMailboxCounter(db, mailboxData._id, false);
} catch (err) {
// ignore
}
response.total = total;
})()
);
counterOps.push(
(async () => {
try {
unseen = await getMailboxCounter(db, mailboxData._id, 'unseen');
} catch (err) {
// ignore
}
response.unseen = unseen;
})()
);
responses.push(response);
}
if (counterOps.length) {
await Promise.all(counterOps);
}
return res.json({
success: true,
results: responses
});
})
);
server.post(
'/users/:user/mailboxes',
tools.responseWrapper(async (req, res) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
user: Joi.string().hex().lowercase().length(24).required(),
path: Joi.string()
.regex(/\/{2,}|\/$/, { invert: true })
.required(),
hidden: booleanSchema.default(false),
retention: Joi.number().min(0),
sess: sessSchema,
ip: sessIPSchema
});
const result = schema.validate(req.params, {
abortEarly: false,
convert: true
});
if (result.error) {
res.status(400);
return res.json({
error: result.error.message,
code: 'InputValidationError',
details: tools.validationErrors(result)
});
}
// permissions check
if (req.user && req.user === result.value.user) {
req.validate(roles.can(req.role).createOwn('mailboxes'));
} else {
req.validate(roles.can(req.role).createAny('mailboxes'));
}
let user = new ObjectId(result.value.user);
let path = imapTools.normalizeMailbox(result.value.path);
let retention = result.value.retention;
let opts = {
subscribed: true,
hidden: !!result.value.hidden
};
if (retention) {
opts.retention = retention;
}
let status, id;
try {
let data = await createMailbox(user, path, opts);
status = data.status;
id = data.id;
} catch (err) {
res.status(500); // TODO: use response code specific status
return res.json({
error: err.message,
code: err.code
});
}
if (typeof status === 'string') {
res.status(500);
return res.json({
error: 'Mailbox creation failed with code ' + status
});
}
return res.json({
success: !!status,
id
});
})
);
server.get(
'/users/:user/mailboxes/:mailbox',
tools.responseWrapper(async (req, res) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
user: Joi.string().hex().lowercase().length(24).required(),
mailbox: Joi.string().hex().lowercase().length(24).allow('resolve').required(),
path: Joi.string().regex(/\/{2,}|\/$/, { invert: true }),
sess: sessSchema,
ip: sessIPSchema
});
const result = schema.validate(req.params, {
abortEarly: false,
convert: true
});
if (result.error) {
res.status(400);
return res.json({
error: result.error.message,
code: 'InputValidationError',
details: tools.validationErrors(result)
});
}
// permissions check
if (req.user && req.user === result.value.user) {
req.validate(roles.can(req.role).readOwn('mailboxes'));
} else {
req.validate(roles.can(req.role).readAny('mailboxes'));
}
let user = new ObjectId(result.value.user);
let mailbox = result.value.mailbox !== 'resolve' ? new ObjectId(result.value.mailbox) : 'resolve';
let userData;
try {
userData = await db.users.collection('users').findOne(
{
_id: user
},
{
projection: {
address: true
}
}
);
} catch (err) {
res.status(500);
return res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
}
if (!userData) {
res.status(404);
return res.json({
error: 'This user does not exist',
code: 'UserNotFound'
});
}
let mailboxQuery = {
_id: mailbox,
user
};
if (mailbox === 'resolve') {
mailboxQuery = {
path: result.value.path,
user
};
}
let mailboxData;
try {
mailboxData = await db.database.collection('mailboxes').findOne(mailboxQuery);
} catch (err) {
res.status(500);
return res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
}
if (!mailboxData) {
res.status(404);
return res.json({
error: 'This mailbox does not exist',
code: 'NoSuchMailbox'
});
}
mailbox = mailboxData._id;
let path = mailboxData.path.split('/');
let name = path.pop();
let total, unseen;
try {
total = await getMailboxCounter(db, mailboxData._id, false);
} catch (err) {
// ignore
}
try {
unseen = await getMailboxCounter(db, mailboxData._id, 'unseen');
} catch (err) {
// ignore
}
return res.json({
success: true,
id: mailbox,
name,
path: mailboxData.path,
specialUse: mailboxData.specialUse,
modifyIndex: mailboxData.modifyIndex,
subscribed: mailboxData.subscribed,
hidden: !!mailboxData.hidden,
total,
unseen
});
})
);
server.put(
'/users/:user/mailboxes/:mailbox',
tools.responseWrapper(async (req, res) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
user: Joi.string().hex().lowercase().length(24).required(),
mailbox: Joi.string().hex().lowercase().length(24).required(),
path: Joi.string().regex(/\/{2,}|\/$/, { invert: true }),
retention: Joi.number().empty('').min(0),
subscribed: booleanSchema,
hidden: booleanSchema,
sess: sessSchema,
ip: sessIPSchema
});
const result = schema.validate(req.params, {
abortEarly: false,
convert: true
});
if (result.error) {
res.status(400);
return res.json({
error: result.error.message,
code: 'InputValidationError',
details: tools.validationErrors(result)
});
}
// permissions check
if (req.user && req.user === result.value.user) {
req.validate(roles.can(req.role).updateOwn('mailboxes'));
} else {
req.validate(roles.can(req.role).updateAny('mailboxes'));
}
let user = new ObjectId(result.value.user);
let mailbox = new ObjectId(result.value.mailbox);
let updates = {};
let update = false;
Object.keys(result.value || {}).forEach(key => {
if (!['user', 'mailbox'].includes(key)) {
updates[key] = result.value[key];
update = true;
}
});
if (!update) {
res.status(400);
return res.json({
error: 'Nothing was changed'
});
}
let status;
try {
status = await updateMailbox(user, mailbox, updates);
} catch (err) {
res.status(500); // TODO: use response code specific status
return res.json({
error: err.message,
code: err.code
});
}
if (typeof status === 'string') {
res.status(500);
return res.json({
error: 'Mailbox update failed with code ' + status
});
}
return res.json({
success: true
});
})
);
server.del(
'/users/:user/mailboxes/:mailbox',
tools.responseWrapper(async (req, res) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
user: Joi.string().hex().lowercase().length(24).required(),
mailbox: Joi.string().hex().lowercase().length(24).required(),
sess: sessSchema,
ip: sessIPSchema
});
const result = schema.validate(req.params, {
abortEarly: false,
convert: true
});
if (result.error) {
res.status(400);
return res.json({
error: result.error.message,
code: 'InputValidationError',
details: tools.validationErrors(result)
});
}
// permissions check
if (req.user && req.user === result.value.user) {
req.validate(roles.can(req.role).deleteOwn('mailboxes'));
} else {
req.validate(roles.can(req.role).deleteAny('mailboxes'));
}
let user = new ObjectId(result.value.user);
let mailbox = new ObjectId(result.value.mailbox);
let status;
try {
status = await deleteMailbox(user, mailbox);
} catch (err) {
res.status(500); // TODO: use response code specific status
return res.json({
error: err.message,
code: err.code
});
}
if (typeof status === 'string') {
res.status(500); // TODO: use response code specific status
return res.json({
error: 'Mailbox deletion failed with code ' + status,
code: status
});
}
return res.json({
success: true
});
})
);
};