2017-07-16 00:08:33 +08:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
const db = require('../db');
|
2017-07-17 04:04:59 +08:00
|
|
|
const tools = require('../tools');
|
2019-01-26 18:50:51 +08:00
|
|
|
const consts = require('../consts');
|
2017-07-16 00:08:33 +08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns an array of matching UID values
|
|
|
|
*/
|
2017-12-10 07:19:50 +08:00
|
|
|
module.exports = server => (mailbox, options, session, callback) => {
|
2017-11-28 20:49:29 +08:00
|
|
|
db.database.collection('mailboxes').findOne(
|
|
|
|
{
|
2017-12-10 07:19:50 +08:00
|
|
|
_id: mailbox
|
2017-11-28 20:49:29 +08:00
|
|
|
},
|
2019-01-24 05:38:29 +08:00
|
|
|
{
|
2019-01-26 18:50:51 +08:00
|
|
|
maxTimeMS: consts.DB_MAX_TIME_MAILBOXES
|
2019-01-24 05:38:29 +08:00
|
|
|
},
|
2017-11-28 20:49:29 +08:00
|
|
|
(err, mailboxData) => {
|
|
|
|
if (err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
if (!mailboxData) {
|
|
|
|
return callback(null, 'NONEXISTENT');
|
|
|
|
}
|
|
|
|
|
|
|
|
// prepare query
|
|
|
|
|
|
|
|
let query = {
|
|
|
|
mailbox: mailboxData._id
|
|
|
|
};
|
2017-08-11 03:20:21 +08:00
|
|
|
|
2021-04-27 18:10:08 +08:00
|
|
|
let returned = false;
|
2017-11-28 20:49:29 +08:00
|
|
|
let walkQuery = (parent, ne, node) => {
|
2021-04-27 18:10:08 +08:00
|
|
|
if (returned) {
|
|
|
|
return;
|
|
|
|
}
|
2017-11-28 20:49:29 +08:00
|
|
|
node.forEach(term => {
|
|
|
|
switch (term.key) {
|
|
|
|
case 'all':
|
|
|
|
if (ne) {
|
|
|
|
parent.push({
|
|
|
|
// should not match anything
|
|
|
|
_id: -1
|
|
|
|
});
|
|
|
|
}
|
|
|
|
break;
|
2017-07-16 00:08:33 +08:00
|
|
|
|
2017-11-28 20:49:29 +08:00
|
|
|
case 'not':
|
|
|
|
walkQuery(parent, !ne, [].concat(term.value || []));
|
|
|
|
break;
|
2017-07-16 00:08:33 +08:00
|
|
|
|
2017-11-28 20:49:29 +08:00
|
|
|
case 'or': {
|
|
|
|
let $or = [];
|
2017-07-16 00:08:33 +08:00
|
|
|
|
2017-11-28 20:49:29 +08:00
|
|
|
[].concat(term.value || []).forEach(entry => {
|
|
|
|
walkQuery($or, false, [].concat(entry || []));
|
2017-07-16 00:08:33 +08:00
|
|
|
});
|
|
|
|
|
2017-11-28 20:49:29 +08:00
|
|
|
if ($or.length) {
|
|
|
|
parent.push({
|
|
|
|
$or
|
|
|
|
});
|
|
|
|
}
|
2017-07-16 00:08:33 +08:00
|
|
|
|
2017-11-28 20:49:29 +08:00
|
|
|
break;
|
2017-07-16 00:08:33 +08:00
|
|
|
}
|
|
|
|
|
2017-11-28 20:49:29 +08:00
|
|
|
case 'text': // search over entire email
|
|
|
|
case 'body': // search over email body
|
|
|
|
if (term.value && !ne) {
|
|
|
|
// fulltext can only be in the root of the query, not in $not, $or expressions
|
|
|
|
// https://docs.mongodb.com/v3.4/tutorial/text-search-in-aggregation/#restrictions
|
|
|
|
query.user = session.user.id;
|
|
|
|
query.searchable = true;
|
|
|
|
query.$text = {
|
2019-04-29 21:18:45 +08:00
|
|
|
$search: term.value
|
2017-11-28 20:49:29 +08:00
|
|
|
};
|
|
|
|
} else {
|
|
|
|
// can not search by text
|
2017-07-16 00:08:33 +08:00
|
|
|
parent.push({
|
|
|
|
// should not match anything
|
|
|
|
_id: -1
|
|
|
|
});
|
|
|
|
}
|
2017-11-28 20:49:29 +08:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 'modseq':
|
2017-07-16 00:08:33 +08:00
|
|
|
parent.push({
|
2017-11-28 20:49:29 +08:00
|
|
|
modseq: {
|
|
|
|
[!ne ? '$gte' : '$lt']: term.value
|
2017-07-16 00:08:33 +08:00
|
|
|
}
|
|
|
|
});
|
2017-11-28 20:49:29 +08:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 'uid':
|
|
|
|
if (Array.isArray(term.value)) {
|
|
|
|
if (!term.value.length) {
|
|
|
|
// trying to find a message that does not exist
|
2021-04-27 18:10:08 +08:00
|
|
|
returned = true;
|
2017-11-28 20:49:29 +08:00
|
|
|
return callback(null, {
|
|
|
|
uidList: [],
|
|
|
|
highestModseq: 0
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (term.value.length !== session.selected.uidList.length) {
|
|
|
|
// not 1:*
|
|
|
|
parent.push({
|
|
|
|
uid: tools.checkRangeQuery(term.value, ne)
|
|
|
|
});
|
|
|
|
} else if (ne) {
|
|
|
|
parent.push({
|
|
|
|
// should not match anything
|
|
|
|
_id: -1
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
parent.push({
|
|
|
|
uid: {
|
|
|
|
[!ne ? '$eq' : '$ne']: term.value
|
2017-07-16 00:08:33 +08:00
|
|
|
}
|
2017-11-28 20:49:29 +08:00
|
|
|
});
|
2017-07-16 00:08:33 +08:00
|
|
|
}
|
2017-11-28 20:49:29 +08:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 'flag':
|
|
|
|
{
|
|
|
|
switch (term.value) {
|
|
|
|
case '\\Seen':
|
|
|
|
case '\\Deleted':
|
|
|
|
// message object has "unseen" and "undeleted" properties
|
|
|
|
if (term.exists) {
|
|
|
|
parent.push({
|
|
|
|
['un' + term.value.toLowerCase().substr(1)]: ne
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
parent.push({
|
|
|
|
['un' + term.value.toLowerCase().substr(1)]: !ne
|
|
|
|
});
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case '\\Flagged':
|
|
|
|
case '\\Draft':
|
|
|
|
if (term.exists) {
|
|
|
|
parent.push({
|
|
|
|
[term.value.toLowerCase().substr(1)]: !ne
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
parent.push({
|
|
|
|
[term.value.toLowerCase().substr(1)]: ne
|
|
|
|
});
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
if (term.exists) {
|
|
|
|
parent.push({
|
|
|
|
flags: {
|
|
|
|
[!ne ? '$eq' : '$ne']: term.value
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
parent.push({
|
|
|
|
flags: {
|
|
|
|
[!ne ? '$ne' : '$eq']: term.value
|
2017-07-16 00:08:33 +08:00
|
|
|
}
|
2017-11-28 20:49:29 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'header':
|
|
|
|
{
|
|
|
|
let regex = Buffer.from(term.value, 'binary')
|
|
|
|
.toString()
|
|
|
|
.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
|
|
let entry = term.value
|
|
|
|
? {
|
2019-01-24 05:38:29 +08:00
|
|
|
headers: {
|
|
|
|
$elemMatch: {
|
|
|
|
key: term.header,
|
|
|
|
value: !ne
|
|
|
|
? {
|
|
|
|
$regex: regex,
|
|
|
|
$options: 'i'
|
2017-11-28 20:49:29 +08:00
|
|
|
}
|
2019-01-24 05:38:29 +08:00
|
|
|
: {
|
|
|
|
// not can not have a regex, so try exact match instead even if it fails
|
|
|
|
$not: {
|
2020-09-10 15:27:50 +08:00
|
|
|
$eq: Buffer.from(term.value, 'binary').toString().toLowerCase().trim()
|
2019-01-24 05:38:29 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-11-28 20:49:29 +08:00
|
|
|
: {
|
2019-01-24 05:38:29 +08:00
|
|
|
'headers.key': !ne
|
|
|
|
? term.header
|
|
|
|
: {
|
|
|
|
$ne: term.header
|
|
|
|
}
|
|
|
|
};
|
2017-11-28 20:49:29 +08:00
|
|
|
parent.push(entry);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'internaldate':
|
|
|
|
{
|
|
|
|
let op = false;
|
|
|
|
let value = new Date(term.value + ' GMT');
|
|
|
|
switch (term.operator) {
|
|
|
|
case '<':
|
|
|
|
op = '$lt';
|
|
|
|
break;
|
|
|
|
case '<=':
|
|
|
|
op = '$lte';
|
|
|
|
break;
|
|
|
|
case '>':
|
|
|
|
op = '$gt';
|
|
|
|
break;
|
|
|
|
case '>=':
|
|
|
|
op = '$gte';
|
|
|
|
break;
|
2017-07-16 00:08:33 +08:00
|
|
|
}
|
2017-11-28 20:49:29 +08:00
|
|
|
let entry = !op
|
|
|
|
? [
|
2019-01-24 05:38:29 +08:00
|
|
|
{
|
|
|
|
$gte: value
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$lt: new Date(value.getTime() + 24 * 3600 * 1000)
|
|
|
|
}
|
|
|
|
]
|
2017-11-28 20:49:29 +08:00
|
|
|
: {
|
2019-01-24 05:38:29 +08:00
|
|
|
[op]: value
|
|
|
|
};
|
2017-11-28 20:49:29 +08:00
|
|
|
|
|
|
|
entry = {
|
|
|
|
idate: !ne
|
|
|
|
? entry
|
2017-07-16 00:08:33 +08:00
|
|
|
: {
|
2019-01-24 05:38:29 +08:00
|
|
|
$not: entry
|
|
|
|
}
|
2017-07-16 00:08:33 +08:00
|
|
|
};
|
|
|
|
|
2017-11-28 20:49:29 +08:00
|
|
|
parent.push(entry);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'headerdate':
|
|
|
|
{
|
|
|
|
let op = false;
|
|
|
|
let value = new Date(term.value + ' GMT');
|
|
|
|
switch (term.operator) {
|
|
|
|
case '<':
|
|
|
|
op = '$lt';
|
|
|
|
break;
|
|
|
|
case '<=':
|
|
|
|
op = '$lte';
|
|
|
|
break;
|
|
|
|
case '>':
|
|
|
|
op = '$gt';
|
|
|
|
break;
|
|
|
|
case '>=':
|
|
|
|
op = '$gte';
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
let entry = !op
|
|
|
|
? [
|
2019-01-24 05:38:29 +08:00
|
|
|
{
|
|
|
|
$gte: value
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$lt: new Date(value.getTime() + 24 * 3600 * 1000)
|
|
|
|
}
|
|
|
|
]
|
2017-07-16 00:08:33 +08:00
|
|
|
: {
|
2019-01-24 05:38:29 +08:00
|
|
|
[op]: value
|
|
|
|
};
|
2017-07-16 00:08:33 +08:00
|
|
|
|
2017-11-28 20:49:29 +08:00
|
|
|
entry = {
|
|
|
|
hdate: !ne
|
|
|
|
? entry
|
|
|
|
: {
|
2019-01-24 05:38:29 +08:00
|
|
|
$not: entry
|
|
|
|
}
|
2017-11-28 20:49:29 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
parent.push(entry);
|
2017-07-16 00:08:33 +08:00
|
|
|
}
|
2017-11-28 20:49:29 +08:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 'size':
|
|
|
|
{
|
|
|
|
let op = '$eq';
|
|
|
|
let value = Number(term.value) || 0;
|
|
|
|
switch (term.operator) {
|
|
|
|
case '<':
|
|
|
|
op = '$lt';
|
|
|
|
break;
|
|
|
|
case '<=':
|
|
|
|
op = '$lte';
|
|
|
|
break;
|
|
|
|
case '>':
|
|
|
|
op = '$gt';
|
|
|
|
break;
|
|
|
|
case '>=':
|
|
|
|
op = '$gte';
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
let entry = {
|
2017-07-16 00:08:33 +08:00
|
|
|
[op]: value
|
|
|
|
};
|
|
|
|
|
2017-11-28 20:49:29 +08:00
|
|
|
entry = {
|
|
|
|
size: !ne
|
|
|
|
? entry
|
|
|
|
: {
|
2019-01-24 05:38:29 +08:00
|
|
|
$not: entry
|
|
|
|
}
|
2017-11-28 20:49:29 +08:00
|
|
|
};
|
2017-07-16 00:08:33 +08:00
|
|
|
|
2017-11-28 20:49:29 +08:00
|
|
|
parent.push(entry);
|
2017-07-16 00:08:33 +08:00
|
|
|
}
|
2017-11-28 20:49:29 +08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
2017-07-16 00:08:33 +08:00
|
|
|
|
2017-11-28 20:49:29 +08:00
|
|
|
let $and = [];
|
|
|
|
walkQuery($and, false, options.query);
|
2021-04-27 18:10:08 +08:00
|
|
|
if (returned) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-11-28 20:49:29 +08:00
|
|
|
if ($and.length) {
|
|
|
|
query.$and = $and;
|
|
|
|
}
|
|
|
|
|
|
|
|
server.logger.info(
|
|
|
|
{
|
|
|
|
tnx: 'search',
|
|
|
|
cid: session.id
|
|
|
|
},
|
|
|
|
'[%s] SEARCH %s',
|
|
|
|
session.id,
|
|
|
|
JSON.stringify(query)
|
|
|
|
);
|
|
|
|
|
|
|
|
let cursor = db.database
|
|
|
|
.collection('messages')
|
|
|
|
.find(query)
|
|
|
|
.project({
|
|
|
|
uid: true,
|
|
|
|
modseq: true
|
2019-01-24 05:38:29 +08:00
|
|
|
})
|
2021-08-30 16:18:42 +08:00
|
|
|
.withReadPreference('secondaryPreferred')
|
2019-01-26 18:50:51 +08:00
|
|
|
.maxTimeMS(consts.DB_MAX_TIME_MESSAGES);
|
2017-11-28 20:49:29 +08:00
|
|
|
|
|
|
|
let highestModseq = 0;
|
|
|
|
let uidList = [];
|
|
|
|
|
|
|
|
let processNext = () => {
|
|
|
|
cursor.next((err, message) => {
|
|
|
|
if (err) {
|
|
|
|
server.logger.error(
|
|
|
|
{
|
|
|
|
tnx: 'search',
|
|
|
|
cid: session.id
|
|
|
|
},
|
|
|
|
'[%s] SEARCHFAIL %s error="%s"',
|
|
|
|
session.id,
|
|
|
|
JSON.stringify(query),
|
|
|
|
err.message
|
|
|
|
);
|
|
|
|
return callback(new Error('Can not make requested search query'));
|
|
|
|
}
|
|
|
|
if (!message) {
|
|
|
|
return cursor.close(() =>
|
|
|
|
callback(null, {
|
|
|
|
uidList,
|
|
|
|
highestModseq
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
2017-07-16 00:08:33 +08:00
|
|
|
|
2017-11-28 20:49:29 +08:00
|
|
|
if (highestModseq < message.modseq) {
|
|
|
|
highestModseq = message.modseq;
|
|
|
|
}
|
2017-07-16 00:08:33 +08:00
|
|
|
|
2017-11-28 20:49:29 +08:00
|
|
|
uidList.push(message.uid);
|
|
|
|
setImmediate(processNext);
|
|
|
|
});
|
2017-08-11 03:20:21 +08:00
|
|
|
};
|
|
|
|
|
2017-11-28 20:49:29 +08:00
|
|
|
setImmediate(processNext);
|
|
|
|
}
|
|
|
|
);
|
2017-07-16 00:08:33 +08:00
|
|
|
};
|