Skip to content

Commit

Permalink
Search by query text
Browse files Browse the repository at this point in the history
  • Loading branch information
andris9 committed Apr 27, 2023
1 parent 68fc341 commit 3f656e8
Show file tree
Hide file tree
Showing 2 changed files with 208 additions and 58 deletions.
17 changes: 16 additions & 1 deletion lib/api/messages.js
Expand Up @@ -21,6 +21,8 @@ const { nextPageCursorSchema, previousPageCursorSchema, pageNrSchema, sessSchema
const { preprocessAttachments } = require('../data-url');
const TaskHandler = require('../task-handler');
const prepareSearchFilter = require('../prepare-search-filter');
const { getMongoDBQuery } = require('../search-query');

const BimiHandler = require('../bimi-handler');

module.exports = (db, server, messageHandler, userHandler, storageHandler, settingsHandler) => {
Expand Down Expand Up @@ -538,6 +540,7 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
res.charSet('utf-8');

const schema = searchSchema.keys({
q: Joi.string().trim().empty('').max(1024).optional(),
threadCounters: booleanSchema.default(false),
limit: Joi.number().default(20).min(1).max(250),
order: Joi.any().empty('').allow('asc', 'desc').optional(),
Expand Down Expand Up @@ -576,7 +579,19 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
let pagePrevious = result.value.previous;
let order = result.value.order;

let { filter, query } = await prepareSearchFilter(db, user, result.value);
let filter;
let query;

if (result.value.q) {
filter = await getMongoDBQuery(db, user, result.value.q);
query = result.value.q;
} else {
let prepared = await prepareSearchFilter(db, user, result.value);
filter = prepared.filter;
query = prepared.query;
}

console.log('SEARCH FILTER', JSON.stringify(filter));

let total = await getFilteredMessageCount(filter);
log.verbose('API', 'Searching %s', JSON.stringify(filter));
Expand Down
249 changes: 192 additions & 57 deletions lib/search-query.js
@@ -1,19 +1,9 @@
'use strict';

const util = require('util');
const SearchString = require('search-string');
const parser = require('logic-query-parser');

const isModifier = node => {
if (node?.left?.lexeme?.type === 'string' && node?.left?.lexeme?.value?.at(-1) === ':' && node?.right) {
let keyword = node.left.lexeme.value.substring(0, node.left.lexeme.value.length - 1);
let negated = keyword.at(0) === '-';
if (negated) {
keyword = keyword.substring(1);
}
return keyword ? { keyword, negated } : null;
}
};
const { escapeRegexStr } = require('./tools');
const { ObjectId } = require('mongodb');

function parseSearchQuery(queryStr) {
const queryTree = parser.parse(queryStr);
Expand All @@ -32,18 +22,19 @@ function parseSearchQuery(queryStr) {
leafNode = node.$and;
}

let queryModifier = isModifier(node);
if (node.left) {
let subLeaf = [];

if (queryModifier) {
walkQueryTree(
node.right,
subLeaf,
Object.assign({}, opts, {
condType: 'and'
})
);
if (
node.left?.lexeme?.type === 'string' &&
typeof node.left.lexeme.value === 'string' &&
node.left.lexeme.value.length > 1 &&
node.left.lexeme.value.at(-1) === ':' &&
node.right?.lexeme?.type === 'and' &&
node.right.left?.lexeme?.type === 'string' &&
node.right.left.lexeme.value
) {
//
node.left.lexeme.value += `"${node.right.left.lexeme.value}"`;
node.right = node.right.right;
}

walkQueryTree(
Expand All @@ -53,26 +44,9 @@ function parseSearchQuery(queryStr) {
condType: 'and'
})
);

if (queryModifier) {
let entry = leafNode.at(-1);

let subEntries = [];
if (entry?.keywords?.[queryModifier.keyword]?.value === '') {
for (let textEntry of subLeaf) {
if (textEntry?.text?.value) {
subEntries.push({
text: null,
keywords: { [queryModifier.keyword]: textEntry?.text?.value, negated: textEntry?.text?.negated }
});
}
}
}

leafNode.splice(-1, 1, ...subEntries);
}
}
if (node.right && !queryModifier) {

if (node.right) {
walkQueryTree(
node.right,
leafNode,
Expand Down Expand Up @@ -162,32 +136,193 @@ function parseSearchQuery(queryStr) {

walkQueryTree(queryTree, result, { condType: 'and' });

console.log(util.inspect(queryTree, false, 22, true));

return {
query: queryStr,
parsed: result
};
return result;
}

const getMongoDBQuery = queryStr => {
const getMongoDBQuery = async (db, user, queryStr) => {
const parsed = parseSearchQuery(queryStr);

const query = {};
let walkTree = async node => {
if (Array.isArray(node)) {
let branches = [];
for (let entry of node) {
branches.push(await walkTree(entry));
}
return branches;
}

if (node.$and && node.$and.length) {
let branch = {
$and: []
};

for (let entry of node.$and) {
let subBranch = await walkTree(entry);
branch.$and = branch.$and.concat(subBranch || []);
}

return branch;
} else if (node.$or && node.$or.length) {
let branch = {
$or: []
};

let walkTree = node => {};
for (let entry of node.$or) {
let subBranch = await walkTree(entry);

branch.$or = branch.$or.concat(subBranch || []);
}

return branch;
} else if (node.text) {
let branch = {
$text: {
$search: node.text.value
}
};

if (node.text.negated) {
branch = { $not: branch };
}

return branch;
} else if (node.keywords) {
let branches = [];

let keyword = Object.keys(node.keywords || {}).find(key => key && key !== 'negated');
if (keyword) {
let { value, negated } = node.keywords[keyword];
switch (keyword) {
case 'from':
case 'subject':
{
let regex = escapeRegexStr(value);
let branch = {
headers: {
$elemMatch: {
key: keyword,
value: {
$regex: regex,
$options: 'i'
}
}
}
};
if (negated) {
branch = { $not: branch };
}
branches.push(branch);
}
break;

case 'to':
{
let regex = escapeRegexStr(value);
for (let toKey of ['to', 'cc', 'bcc']) {
let branch = {
headers: {
$elemMatch: {
key: toKey,
value: {
$regex: regex,
$options: 'i'
}
}
}
};
if (negated) {
branch = { $not: branch };
}
branches.push(branch);
}
}
break;

case 'in': {
value = (value || '').toString().trim();
let resolveQuery = { user, $or: [] };
if (/^[0-9a-f]{24}$/i.test(value)) {
resolveQuery.$or.push({ _id: new ObjectId(value) });
} else if (/^Inbox$/i.test(value)) {
resolveQuery.$or.push({ path: 'INBOX' });
} else {
resolveQuery.$or.push({ path: value });
if (/^\/?(spam|junk)/i.test(value)) {
resolveQuery.$or.push({ specialUse: '\\Junk' });
} else if (/^\/?(sent)/i.test(value)) {
resolveQuery.$or.push({ specialUse: '\\Sent' });
} else if (/^\/?(trash|deleted)/i.test(value)) {
resolveQuery.$or.push({ specialUse: '\\Trash' });
} else if (/^\/?(drafts)/i.test(value)) {
resolveQuery.$or.push({ specialUse: '\\Drafts' });
}
}

let mailboxEntry = await db.database.collection('mailboxes').findOne(resolveQuery, { project: { _id: -1 } });

let branch = { mailbox: mailboxEntry ? mailboxEntry._id : new ObjectId('0'.repeat(24)) };
if (negated) {
branch = { $not: branch };
}
branches.push(branch);

break;
}

case 'thread':
{
value = (value || '').toString().trim();
if (/^[0-9a-f]{24}$/i.test(value)) {
let branch = { thread: new ObjectId(value) };
if (negated) {
branch = { $not: branch };
}
branches.push(branch);
}
}
break;

case 'has': {
switch (value) {
case 'attachment': {
branches.push({ ha: true });
break;
}
}
}
}
}

return branches;
}
};

if (parsed && parsed.length) {
walkTree(parsed[0]);
return Object.assign({ user: null }, await walkTree(Array.isArray(parsed) ? { $and: parsed } : parsed), { user });
}

return query;
return { user: false };
};

module.exports = { parseSearchQuery, getMongoDBQuery };

let queries = ['from:amy to:greg has:attachment subject:"dinner and movie tonight" OR subject:(dinner movie)'];
/*
const util = require('util');
let main = () => {
let db = require('./db');
db.connect(() => {
let run = async () => {
let queries = ['from:"amy namy" kupi in:spam to:greg has:attachment -subject:"dinner and movie tonight" (jupi OR subject:tere)'];
for (let query of queries) {
console.log(util.inspect({ query, parsed: parseSearchQuery(query) }, false, 22, true));
}
for (let query of queries) {
console.log(util.inspect({ query, parsed: parseSearchQuery(query) }, false, 22, true));
console.log(util.inspect({ query, parsed: await getMongoDBQuery(db, new ObjectId('64099fff101ca2ef6aad8be7'), query) }, false, 22, true));
}
};
run();
});
};
main();
*/

0 comments on commit 3f656e8

Please sign in to comment.