wildduck/lib/api/filters.js
2019-07-31 16:05:59 +03:00

1263 lines
45 KiB
JavaScript

'use strict';
const Joi = require('joi');
const ObjectID = require('mongodb').ObjectID;
const urllib = require('url');
const tools = require('../tools');
const roles = require('../roles');
module.exports = (db, server) => {
/**
* @api {get} /users/:user/filters List Filters for a User
* @apiName GetFilters
* @apiGroup Filters
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
* @apiHeaderExample {json} Header-Example:
* {
* "X-Access-Token": "59fc66a03e54454869460e45"
* }
*
* @apiParam {String} user Users unique ID
*
* @apiSuccess {Boolean} success Indicates successful response
* @apiSuccess {Object[]} results Filter description
* @apiSuccess {String} results.id Filter ID
* @apiSuccess {String} results.name Name for the filter
* @apiSuccess {String} results.created Datestring of the time the filter was created
* @apiSuccess {Array[]} results.query A list of query descriptions
* @apiSuccess {Array[]} results.action A list of action descriptions
* @apiSuccess {Boolean} results.disabled If true, then this filter is ignored
*
* @apiError error Description of the error
*
* @apiExample {curl} Example usage:
* curl -i http://localhost:8080/users/5a1bda70bfbd1442cd96c6f0/filters
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "success": true,
* "results": [
* {
* "id": "5a1c0ee490a34c67e266931c",
* "query": [
* [
* "from",
* "(Mäger)"
* ]
* ],
* "action": [
* [
* "mark as read"
* ]
* ],
* "disabled": false,
* "created": "2017-11-27T13:11:00.835Z"
* }
* ]
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 200 OK
* {
* "error": "This user does not exist"
* }
*/
server.get(
'/users/:user/filters',
tools.asyncifyJson(async (req, res, next) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
user: Joi.string()
.hex()
.lowercase()
.length(24)
.required(),
sess: Joi.string().max(255),
ip: Joi.string().ip({
version: ['ipv4', 'ipv6'],
cidr: 'forbidden'
})
});
const result = Joi.validate(req.params, schema, {
abortEarly: false,
convert: true
});
if (result.error) {
res.status(400);
res.json({
error: result.error.message,
code: 'InputValidationError'
});
return next();
}
// permissions check
if (req.user && req.user === result.value.user) {
req.validate(roles.can(req.role).readOwn('filters'));
} else {
req.validate(roles.can(req.role).readAny('filters'));
}
let user = new ObjectID(result.value.user);
let userData;
try {
userData = await db.users.collection('users').findOne(
{
_id: user
},
{
projection: {
address: true
}
}
);
} catch (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
if (!userData) {
res.json({
error: 'This user does not exist',
code: 'UserNotFound'
});
return next();
}
let mailboxes;
try {
mailboxes = await db.database
.collection('mailboxes')
.find({
user
})
.project({ _id: 1, path: 1 })
.sort({ _id: 1 })
.toArray();
} catch (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
if (!mailboxes) {
mailboxes = [];
}
let filters;
try {
filters = await db.database
.collection('filters')
.find({
user
})
.sort({
_id: 1
})
.toArray();
} catch (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
if (!filters) {
filters = [];
}
res.json({
success: true,
results: filters.map(filterData => {
let descriptions = getFilterStrings(filterData, mailboxes);
return {
id: filterData._id,
name: filterData.name,
query: descriptions.query,
action: descriptions.action,
disabled: !!filterData.disabled,
created: filterData.created
};
})
});
return next();
})
);
/**
* @api {get} /users/:user/filters/:filter Request Filter information
* @apiName GetFilter
* @apiGroup Filters
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
* @apiHeaderExample {json} Header-Example:
* {
* "X-Access-Token": "59fc66a03e54454869460e45"
* }
*
* @apiParam {String} user Users unique ID.
* @apiParam {String} filter Filters unique ID.
*
* @apiSuccess {Boolean} success Indicates successful response
* @apiSuccess {String} id ID for the Filter
* @apiSuccess {String} name Name of the Filter
* @apiSuccess {Object} query Rules that a message must match
* @apiSuccess {String} query.from Partial match for the From: header (case insensitive)
* @apiSuccess {String} query.to Partial match for the To:/Cc: headers (case insensitive)
* @apiSuccess {String} query.subject Partial match for the Subject: header (case insensitive)
* @apiSuccess {String} query.listId Partial match for the List-ID: header (case insensitive)
* @apiSuccess {String} query.text Fulltext search against message text
* @apiSuccess {Boolean} query.ha Does a message have to have an attachment or not
* @apiSuccess {Number} query.size Message size in bytes. If the value is a positive number then message needs to be larger, if negative then message needs to be smaller than abs(size) value
* @apiSuccess {Object} action Action to take with a matching message
* @apiSuccess {Boolean} action.seen If true then mark matching messages as Seen
* @apiSuccess {Boolean} action.flag If true then mark matching messages as Flagged
* @apiSuccess {Boolean} action.delete If true then do not store matching messages
* @apiSuccess {Boolean} action.spam If true then store matching messags to Junk Mail folder
* @apiSuccess {String} action.mailbox Mailbox ID to store matching messages to
* @apiSuccess {String[]} action.targets A list of email addresses / HTTP URLs to forward the message to
* @apiSuccess {Boolean} disabled If true, then this filter is ignored
*
* @apiError error Description of the error
*
* @apiExample {curl} Example usage:
* curl -i http://localhost:8080/users/59fc66a03e54454869460e45/filters/5a1c0ee490a34c67e266931c
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "success": true,
* "id": "5a1c0ee490a34c67e266931c",
* "created": "2017-11-27T13:11:00.835Z",
* "query": {
* "from": "Mäger"
* },
* "action": {
* "seen": true
* },
* "disabled": false
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 200 OK
* {
* "error": "This filter does not exist"
* }
*/
server.get(
'/users/:user/filters/:filter',
tools.asyncifyJson(async (req, res, next) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
user: Joi.string()
.hex()
.lowercase()
.length(24)
.required(),
filter: Joi.string()
.hex()
.lowercase()
.length(24)
.required(),
sess: Joi.string().max(255),
ip: Joi.string().ip({
version: ['ipv4', 'ipv6'],
cidr: 'forbidden'
})
});
const result = Joi.validate(req.params, schema, {
abortEarly: false,
convert: true
});
if (result.error) {
res.status(400);
res.json({
error: result.error.message,
code: 'InputValidationError'
});
return next();
}
// permissions check
if (req.user && req.user === result.value.user) {
req.validate(roles.can(req.role).readOwn('filters'));
} else {
req.validate(roles.can(req.role).readAny('filters'));
}
let user = new ObjectID(result.value.user);
let filter = new ObjectID(result.value.filter);
let filterData;
try {
filterData = await db.database.collection('filters').findOne({
_id: filter,
user
});
} catch (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
if (!filterData) {
res.json({
error: 'This filter does not exist',
code: 'FilterNotFound'
});
return next();
}
let mailboxes;
try {
mailboxes = await db.database
.collection('mailboxes')
.find({
user
})
.project({ _id: 1, path: 1 })
.sort({ _id: 1 })
.toArray();
} catch (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
if (!mailboxes) {
mailboxes = [];
}
let response = {
success: true,
id: filterData._id,
name: filterData.name,
query: {},
action: {},
disabled: !!filterData.disabled,
created: filterData.created
};
Object.keys((filterData.query && filterData.query.headers) || {}).forEach(key => {
response.query[key] = filterData.query.headers[key];
});
Object.keys(filterData.query || {}).forEach(key => {
if (key !== 'headers') {
response.query[key] = filterData.query[key];
}
});
Object.keys(filterData.action || {}).forEach(key => {
if (key === 'targets') {
response.action.targets = filterData.action.targets.map(target => target.value);
}
response.action[key] = filterData.action[key];
});
res.json(response);
return next();
})
);
/**
* @api {delete} /users/:user/filters/:filter Delete a Filter
* @apiName DeleteFilter
* @apiGroup Filters
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
* @apiHeaderExample {json} Header-Example:
* {
* "X-Access-Token": "59fc66a03e54454869460e45"
* }
*
* @apiParam {String} user Users unique ID
* @apiParam {String} filter Filters unique ID
*
* @apiSuccess {Boolean} success Indicates successful response
*
* @apiError error Description of the error
*
* @apiExample {curl} Example usage:
* curl -i -XDELETE http://localhost:8080/users/59fc66a03e54454869460e45/filters/5a1c0ee490a34c67e266931c
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "success": true
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 200 OK
* {
* "error": "This filter does not exist"
* }
*/
server.del(
'/users/:user/filters/:filter',
tools.asyncifyJson(async (req, res, next) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
user: Joi.string()
.hex()
.lowercase()
.length(24)
.required(),
filter: Joi.string()
.hex()
.lowercase()
.length(24)
.required(),
sess: Joi.string().max(255),
ip: Joi.string().ip({
version: ['ipv4', 'ipv6'],
cidr: 'forbidden'
})
});
const result = Joi.validate(req.params, schema, {
abortEarly: false,
convert: true
});
if (result.error) {
res.status(400);
res.json({
error: result.error.message,
code: 'InputValidationError'
});
return next();
}
// permissions check
if (req.user && req.user === result.value.user) {
req.validate(roles.can(req.role).deleteOwn('filters'));
} else {
req.validate(roles.can(req.role).deleteAny('filters'));
}
let user = new ObjectID(result.value.user);
let filter = new ObjectID(result.value.filter);
let r;
try {
r = await db.database.collection('filters').deleteOne({
_id: filter,
user
});
} catch (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
if (!r.deletedCount) {
res.status(404);
res.json({
error: 'Filter was not found'
});
return next();
}
res.json({
success: true
});
return next();
})
);
/**
* @api {post} /users/:user/filters Create new Filter
* @apiName PostFilter
* @apiGroup Filters
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
* @apiHeaderExample {json} Header-Example:
* {
* "X-Access-Token": "59fc66a03e54454869460e45"
* }
*
* @apiParam {String} user Users unique ID.
* @apiParam {String} [name] Name of the Filter
* @apiParam {Object} query Rules that a message must match
* @apiParam {String} [query.from] Partial match for the From: header (case insensitive)
* @apiParam {String} [query.to] Partial match for the To:/Cc: headers (case insensitive)
* @apiParam {String} [query.subject] Partial match for the Subject: header (case insensitive)
* @apiParam {String} [query.listId] Partial match for the List-ID: header (case insensitive)
* @apiParam {String} [query.text] Fulltext search against message text
* @apiParam {Boolean} [query.ha] Does a message have to have an attachment or not
* @apiParam {Number} [query.size] Message size in bytes. If the value is a positive number then message needs to be larger, if negative then message needs to be smaller than abs(size) value
* @apiParam {Object} action Action to take with a matching message
* @apiParam {Boolean} [action.seen] If true then mark matching messages as Seen
* @apiParam {Boolean} [action.flag] If true then mark matching messages as Flagged
* @apiParam {Boolean} [action.delete] If true then do not store matching messages
* @apiParam {Boolean} [action.spam] If true then store matching messags to Junk Mail folder
* @apiParam {String} [action.mailbox] Mailbox ID to store matching messages to
* @apiParam {String[]} [action.targets] An array of forwarding targets. The value could either be an email address or a relay url to next MX server ("smtp://mx2.zone.eu:25") or an URL where mail contents are POSTed to
* @apiParam {Boolean} [disabled] If true then this filter is ignored
*
* @apiSuccess {Boolean} success Indicates successful response
* @apiSuccess {String} id ID for the created Filter
*
* @apiError error Description of the error
*
* @apiExample {curl} Example usage:
* curl -i -XPOST http://localhost:8080/users/5a1bda70bfbd1442cd96c6f0/filters \
* -H 'Content-type: application/json' \
* -d '{
* "query": {
* "from": "Mäger"
* },
* "action": {
* "seen": true
* }
* }'
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "success": true,
* "id": "5a1c0ee490a34c67e266931c"
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 200 OK
* {
* "error": "Empty filter query"
* }
*/
server.post(
'/users/:user/filters',
tools.asyncifyJson(async (req, res, next) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
user: Joi.string()
.hex()
.lowercase()
.length(24)
.required(),
name: Joi.string()
.trim()
.max(255)
.empty(''),
query: Joi.object()
.keys({
from: Joi.string()
.trim()
.max(255)
.empty(''),
to: Joi.string()
.trim()
.max(255)
.empty(''),
subject: Joi.string()
.trim()
.max(255)
.empty(''),
listId: Joi.string()
.trim()
.max(255)
.empty(''),
text: Joi.string()
.trim()
.max(255)
.empty(''),
ha: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', '1', 1])
.falsy(['N', 'false', 'no', 'off', '0', 0, ''])
.empty(''),
size: Joi.number().empty('')
})
.default({}),
action: Joi.object()
.keys({
seen: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', '1', 1])
.falsy(['N', 'false', 'no', 'off', '0', 0, ''])
.empty(''),
flag: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', '1', 1])
.falsy(['N', 'false', 'no', 'off', '0', 0, ''])
.empty(''),
delete: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', '1', 1])
.falsy(['N', 'false', 'no', 'off', '0', 0, ''])
.empty(''),
spam: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', '1', 1])
.falsy(['N', 'false', 'no', 'off', '0', 0, ''])
.empty(''),
mailbox: Joi.string()
.hex()
.lowercase()
.length(24)
.empty(''),
targets: Joi.array()
.items(
Joi.string().email(),
Joi.string().uri({
scheme: [/smtps?/, /https?/],
allowRelative: false,
relativeOnly: false
})
)
.empty('')
})
.default({}),
disabled: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', '1', 1])
.falsy(['N', 'false', 'no', 'off', '0', 0, ''])
.default(false),
sess: Joi.string().max(255),
ip: Joi.string().ip({
version: ['ipv4', 'ipv6'],
cidr: 'forbidden'
})
});
const result = Joi.validate(req.params, schema, {
abortEarly: false,
convert: true
});
if (result.error) {
res.status(400);
res.json({
error: result.error.message,
code: 'InputValidationError'
});
return next();
}
// permissions check
if (req.user && req.user === result.value.user) {
req.validate(roles.can(req.role).createOwn('filters'));
} else {
req.validate(roles.can(req.role).createAny('filters'));
}
let user = new ObjectID(result.value.user);
let filterData = {
_id: new ObjectID(),
user,
query: {
headers: {}
},
action: {},
disabled: result.value.disabled,
created: new Date()
};
if (result.value.name) {
filterData.name = result.value.name;
}
['from', 'to', 'subject', 'listId'].forEach(key => {
if (result.value.query[key]) {
filterData.query.headers[key] = result.value.query[key].replace(/\s+/g, ' ');
}
});
if (result.value.query.text) {
filterData.query.text = result.value.query.text.replace(/\s+/g, ' ');
}
if (typeof result.value.query.ha === 'boolean') {
filterData.query.ha = result.value.query.ha;
}
if (result.value.query.size) {
filterData.query.size = result.value.query.size;
}
['seen', 'flag', 'delete', 'spam'].forEach(key => {
if (typeof result.value.action[key] === 'boolean') {
filterData.action[key] = result.value.action[key];
}
});
let targets = result.value.action.targets;
if (targets) {
for (let i = 0, len = targets.length; i < len; i++) {
let target = targets[i];
if (!/^smtps?:/i.test(target) && !/^https?:/i.test(target) && target.indexOf('@') >= 0) {
// email
targets[i] = {
id: new ObjectID(),
type: 'mail',
value: target
};
} else if (/^smtps?:/i.test(target)) {
targets[i] = {
id: new ObjectID(),
type: 'relay',
value: target
};
} else if (/^https?:/i.test(target)) {
targets[i] = {
id: new ObjectID(),
type: 'http',
value: target
};
} else {
res.json({
error: 'Unknown target type "' + target + '"',
code: 'InputValidationError'
});
return next();
}
}
filterData.action.targets = targets;
}
if (result.value.action.mailbox) {
let mailboxData;
try {
mailboxData = await db.database.collection('mailboxes').findOne({
_id: new ObjectID(result.value.action.mailbox),
user
});
} catch (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
if (!mailboxData) {
res.json({
error: 'This mailbox does not exist',
code: 'NoSuchMailbox'
});
return next();
}
filterData.action.mailbox = mailboxData._id;
}
let userData;
try {
userData = await db.users.collection('users').findOne(
{
_id: user
},
{
projection: {
_id: true
}
}
);
} catch (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
if (!userData) {
res.json({
error: 'This user does not exist',
code: 'UserNotFound'
});
return next();
}
let r;
try {
r = await db.database.collection('filters').insertOne(filterData);
} catch (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
res.json({
success: !!r.insertedCount,
id: filterData._id
});
return next();
})
);
/**
* @api {put} /users/:user/filters/:filter Update Filter information
* @apiName PutFilter
* @apiGroup Filters
* @apiDescription This method updates Filter data. To unset a value, use empty strings
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
* @apiHeaderExample {json} Header-Example:
* {
* "X-Access-Token": "59fc66a03e54454869460e45"
* }
*
* @apiParam {String} user Users unique ID.
* @apiParam {String} filter Filters unique ID.
* @apiParam {String} [name] Name of the Filter
* @apiParam {Object} query Rules that a message must match
* @apiParam {String} [query.from] Partial match for the From: header (case insensitive)
* @apiParam {String} [query.to] Partial match for the To:/Cc: headers (case insensitive)
* @apiParam {String} [query.subject] Partial match for the Subject: header (case insensitive)
* @apiParam {String} [query.listId] Partial match for the List-ID: header (case insensitive)
* @apiParam {String} [query.text] Fulltext search against message text
* @apiParam {Boolean} [query.ha] Does a message have to have an attachment or not
* @apiParam {Number} [query.size] Message size in bytes. If the value is a positive number then message needs to be larger, if negative then message needs to be smaller than abs(size) value
* @apiParam {Object} action Action to take with a matching message
* @apiParam {Boolean} [action.seen] If true then mark matching messages as Seen
* @apiParam {Boolean} [action.flag] If true then mark matching messages as Flagged
* @apiParam {Boolean} [action.delete] If true then do not store matching messages
* @apiParam {Boolean} [action.spam] If true then store matching messags to Junk Mail folder
* @apiParam {String} [action.mailbox] Mailbox ID to store matching messages to
* @apiParam {String[]} [action.targets] An array of forwarding targets. The value could either be an email address or a relay url to next MX server ("smtp://mx2.zone.eu:25") or an URL where mail contents are POSTed to
* @apiParam {Boolean} [disabled] If true then this filter is ignored
*
* @apiSuccess {Boolean} success Indicates successful response
* @apiSuccess {String} id ID for the created Filter
*
* @apiError error Description of the error
*
* @apiExample {curl} Example usage:
* curl -i -XPUT http://localhost:8080/users/59fc66a03e54454869460e45/filters/5a1c0ee490a34c67e266931c \
* -H 'Content-type: application/json' \
* -d '{
* "action": {
* "seen": "",
* "flag": true
* }
* }'
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "success": true
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 200 OK
* {
* "error": "Empty filter query"
* }
*/
server.put(
'/users/:user/filters/:filter',
tools.asyncifyJson(async (req, res, next) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
user: Joi.string()
.hex()
.lowercase()
.length(24)
.required(),
filter: Joi.string()
.hex()
.lowercase()
.length(24)
.required(),
name: Joi.string()
.trim()
.max(255)
.empty(''),
query: Joi.object()
.keys({
from: Joi.string()
.trim()
.max(255)
.empty(''),
to: Joi.string()
.trim()
.max(255)
.empty(''),
subject: Joi.string()
.trim()
.max(255)
.empty(''),
listId: Joi.string()
.trim()
.max(255)
.empty(''),
text: Joi.string()
.trim()
.max(255)
.empty(''),
ha: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', '1', 1])
.falsy(['N', 'false', 'no', 'off', '0', 0, ''])
.empty(''),
size: Joi.number().empty('')
})
.default({}),
action: Joi.object()
.keys({
seen: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', '1', 1])
.falsy(['N', 'false', 'no', 'off', '0', 0, ''])
.empty(''),
flag: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', '1', 1])
.falsy(['N', 'false', 'no', 'off', '0', 0, ''])
.empty(''),
delete: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', '1', 1])
.falsy(['N', 'false', 'no', 'off', '0', 0, ''])
.empty(''),
spam: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', '1', 1])
.falsy(['N', 'false', 'no', 'off', '0', 0, ''])
.empty(''),
mailbox: Joi.string()
.hex()
.lowercase()
.length(24)
.empty(''),
targets: Joi.array()
.items(
Joi.string().email(),
Joi.string().uri({
scheme: [/smtps?/, /https?/],
allowRelative: false,
relativeOnly: false
})
)
.empty('')
})
.default({}),
disabled: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', '1', 1])
.falsy(['N', 'false', 'no', 'off', '0', 0, ''])
.empty(''),
sess: Joi.string().max(255),
ip: Joi.string().ip({
version: ['ipv4', 'ipv6'],
cidr: 'forbidden'
})
});
const result = Joi.validate(req.params, schema, {
abortEarly: false,
convert: true
});
if (result.error) {
res.status(400);
res.json({
error: result.error.message,
code: 'InputValidationError'
});
return next();
}
// permissions check
if (req.user && req.user === result.value.user) {
req.validate(roles.can(req.role).updateOwn('filters'));
} else {
req.validate(roles.can(req.role).updateAny('filters'));
}
let user = new ObjectID(result.value.user);
let filter = new ObjectID(result.value.filter);
let $set = {};
let $unset = {};
let hasChanges = false;
if (result.value.name) {
$set.name = result.value.name;
hasChanges = true;
}
if (typeof result.value.disabled === 'boolean') {
$set.disabled = result.value.disabled;
hasChanges = true;
}
['from', 'to', 'subject', 'listId'].forEach(key => {
if (result.value.query[key]) {
$set['query.headers.' + key] = result.value.query[key].replace(/\s+/g, ' ');
hasChanges = true;
} else if (key in req.params.query) {
// delete empty values
$unset['query.headers.' + key] = true;
hasChanges = true;
}
});
if (result.value.query.text) {
$set['query.text'] = result.value.query.text.replace(/\s+/g, ' ');
hasChanges = true;
} else if ('text' in req.params.query) {
$unset['query.text'] = true;
hasChanges = true;
}
if (typeof result.value.query.ha === 'boolean') {
$set['query.ha'] = result.value.query.ha;
hasChanges = true;
} else if ('ha' in req.params.query) {
$unset['query.ha'] = true;
hasChanges = true;
}
if (result.value.query.size) {
$set['query.size'] = result.value.query.size;
hasChanges = true;
} else if ('size' in req.params.query) {
$unset['query.size'] = true;
hasChanges = true;
}
['seen', 'flag', 'delete', 'spam'].forEach(key => {
if (typeof result.value.action[key] === 'boolean') {
$set['action.' + key] = result.value.action[key];
hasChanges = true;
} else if (key in req.params.action) {
$unset['action.' + key] = true;
hasChanges = true;
}
});
let targets = result.value.action.targets;
if (targets) {
for (let i = 0, len = targets.length; i < len; i++) {
let target = targets[i];
if (!/^smtps?:/i.test(target) && !/^https?:/i.test(target) && target.indexOf('@') >= 0) {
// email
targets[i] = {
id: new ObjectID(),
type: 'mail',
value: target
};
} else if (/^smtps?:/i.test(target)) {
targets[i] = {
id: new ObjectID(),
type: 'relay',
value: target
};
} else if (/^https?:/i.test(target)) {
targets[i] = {
id: new ObjectID(),
type: 'http',
value: target
};
} else {
res.json({
error: 'Unknown target type "' + target + '"',
code: 'InputValidationError'
});
return next();
}
}
$set['action.targets'] = targets;
hasChanges = true;
} else if ('targets' in req.params.action) {
$unset['action.targets'] = true;
hasChanges = true;
}
if (result.value.action) {
if (!result.value.action.mailbox) {
if ('mailbox' in req.params.action) {
// clear target mailbox
$unset['action.mailbox'] = true;
hasChanges = true;
}
} else {
let mailboxData;
try {
mailboxData = await db.database.collection('mailboxes').findOne({
_id: new ObjectID(result.value.action.mailbox),
user
});
} catch (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
if (!mailboxData) {
res.json({
error: 'This mailbox does not exist',
code: 'NoSuchMailbox'
});
return next();
}
$set['action.mailbox'] = mailboxData._id;
hasChanges = true;
}
}
if (!hasChanges) {
res.json({
error: 'No changes'
});
return next();
}
let update = {};
if (Object.keys($set).length) {
update.$set = $set;
}
if (Object.keys($unset).length) {
update.$unset = $unset;
}
let r;
try {
r = await db.database.collection('filters').findOneAndUpdate({ _id: filter, user }, update);
} catch (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
if (!r || !r.value || !r.value._id) {
res.json({
error: 'Filter was not found',
code: 'FilterNotFound'
});
return next();
}
res.json({
success: true
});
return next();
})
);
};
function getFilterStrings(filter, mailboxes) {
let query = Object.keys(filter.query.headers || {}).map(key => [key, '(' + filter.query.headers[key] + ')']);
if (filter.query.ha && filter.query.ha > 0) {
query.push(['has attachment']);
} else if (filter.query.ha && filter.query.ha < 0) {
query.push(['no attachments']);
}
if (filter.query.text) {
query.push([false, '"' + filter.query.text + '"']);
}
if (filter.query.size) {
// let unit = 'B';
let size = Math.abs(filter.query.size || 0);
if (filter.query.size > 0) {
query.push(['larger', size /*+ unit*/]);
} else if (filter.query.size < 0) {
query.push(['smaller', size /*+ unit*/]);
}
}
// process actions
let action = Object.keys(filter.action || {})
.map(key => {
switch (key) {
case 'seen':
if (filter.action[key]) {
return ['mark as read'];
} else {
return ['do not mark as read'];
}
case 'flag':
if (filter.action[key]) {
return ['flag it'];
} else {
return ['do not flag it'];
}
case 'spam':
if (filter.action[key]) {
return ['mark it as spam'];
} else {
return ['do not mark it as spam'];
}
case 'delete':
if (filter.action[key]) {
return ['delete it'];
} else {
return ['do not delete it'];
}
case 'mailbox':
if (filter.action[key]) {
let target = mailboxes.find(mailbox => mailbox._id.toString() === filter.action[key].toString());
return ['move to folder', target ? '"' + target.path + '"' : '?'];
} else {
return ['keep in INBOX'];
}
case 'targets':
if (filter.action[key]) {
return [
'forward to',
filter.action[key]
.map(target => {
switch (target.type) {
case 'http': {
let parsed = urllib.parse(target.value);
return parsed.hostname || parsed.host;
}
default:
return target.value;
}
})
.join(', ')
];
}
break;
}
return false;
})
.filter(str => str);
return {
query,
action
};
}