From a4ae3d7113758d51a9ab04f6ea0bf97fbbcc48c2 Mon Sep 17 00:00:00 2001 From: Louis Laureys Date: Tue, 2 Jan 2024 12:23:43 +0100 Subject: [PATCH] feat(api-search): Allow searching for messages by uid (#587) --- docs/api/openapi.yml | 8 ++++++ lib/api/messages.js | 45 ++++++++------------------------ lib/prepare-search-filter.js | 50 +++++++++++++++++++++++++++++++++++- lib/tasks/search-apply.js | 2 +- 4 files changed, 68 insertions(+), 37 deletions(-) diff --git a/docs/api/openapi.yml b/docs/api/openapi.yml index bbc1000a..c05a493e 100644 --- a/docs/api/openapi.yml +++ b/docs/api/openapi.yml @@ -1945,6 +1945,11 @@ paths: description: ID of the Mailbox schema: type: string + - name: id + in: query + description: Message ID values, only applies when used in combination with `mailbox`. Either comma separated numbers (1,2,3) or colon separated range (3:15), or a range from UID to end (3:*) + schema: + type: string - name: thread in: query description: Thread ID @@ -2091,6 +2096,9 @@ paths: mailbox: description: ID of the Mailbox type: string + id: + description: Message ID values, only applies when used in combination with `mailbox`. Either comma separated numbers (1,2,3) or colon separated range (3:15), or a range from UID to end (3:*) + type: string thread: description: Thread ID type: string diff --git a/lib/api/messages.js b/lib/api/messages.js index 47ce63dc..1f65542c 100644 --- a/lib/api/messages.js +++ b/lib/api/messages.js @@ -20,7 +20,7 @@ const roles = require('../roles'); const { nextPageCursorSchema, previousPageCursorSchema, pageNrSchema, sessSchema, sessIPSchema, booleanSchema, metaDataSchema } = require('../schemas'); const { preprocessAttachments } = require('../data-url'); const TaskHandler = require('../task-handler'); -const prepareSearchFilter = require('../prepare-search-filter'); +const { prepareSearchFilter, uidRangeStringToQuery } = require('../prepare-search-filter'); const { getMongoDBQuery /*, getElasticSearchQuery*/ } = require('../search-query'); //const { getClient } = require('../elasticsearch'); @@ -226,41 +226,9 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti let moveTo = result.value.moveTo ? new ObjectId(result.value.moveTo) : false; let message = result.value.message; - let messageQuery; + let messageQuery = uidRangeStringToQuery(message); - if (/^\d+$/.test(message)) { - messageQuery = Number(message); - } else if (/^\d+(,\d+)*$/.test(message)) { - messageQuery = { - $in: message - .split(',') - .map(uid => Number(uid)) - .sort((a, b) => a - b) - }; - } else if (/^\d+:(\d+|\*)$/.test(message)) { - let parts = message - .split(':') - .map(uid => Number(uid)) - .sort((a, b) => { - if (a === '*') { - return 1; - } - if (b === '*') { - return -1; - } - return a - b; - }); - if (parts[0] === parts[1]) { - messageQuery = parts[0]; - } else { - messageQuery = { - $gte: parts[0] - }; - if (!isNaN(parts[1])) { - messageQuery.$lte = parts[1]; - } - } - } else { + if (!messageQuery) { res.status(404); return res.json({ error: 'Invalid message identifier', @@ -626,6 +594,13 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti q: Joi.string().trim().empty('').max(1024).optional().description('Additional query string'), mailbox: Joi.string().hex().length(24).empty('').description('ID of the Mailbox'), + id: Joi.string() + .trim() + .empty('') + .regex(/^\d+(,\d+)*$|^\d+:(\d+|\*)$/i) + .description( + 'Message ID values, only applies when used in combination with `mailbox`. Either comma separated numbers (1,2,3) or colon separated range (3:15), or a range from UID to end (3:*)' + ), thread: Joi.string().hex().length(24).empty('').description('Thread ID'), or: Joi.object({ diff --git a/lib/prepare-search-filter.js b/lib/prepare-search-filter.js index a1cebf56..65158071 100644 --- a/lib/prepare-search-filter.js +++ b/lib/prepare-search-filter.js @@ -3,8 +3,52 @@ const ObjectId = require('mongodb').ObjectId; const { escapeRegexStr } = require('./tools'); +const uidRangeStringToQuery = uidRange => { + if (!uidRange) { + return; + } + + let query; + + if (/^\d+$/.test(uidRange)) { + query = Number(uidRange); + } else if (/^\d+(,\d+)*$/.test(uidRange)) { + query = { + $in: uidRange + .split(',') + .map(uid => Number(uid)) + .sort((a, b) => a - b) + }; + } else if (/^\d+:(\d+|\*)$/.test(uidRange)) { + let parts = uidRange + .split(':') + .map(uid => Number(uid)) + .sort((a, b) => { + if (a === '*') { + return 1; + } + if (b === '*') { + return -1; + } + return a - b; + }); + if (parts[0] === parts[1]) { + query = parts[0]; + } else { + query = { + $gte: parts[0] + }; + if (!isNaN(parts[1])) { + query.$lte = parts[1]; + } + } + } + return query; +}; + const prepareSearchFilter = async (db, user, payload) => { let mailbox = payload.mailbox ? new ObjectId(payload.mailbox) : false; + let idQuery = uidRangeStringToQuery(payload.id); let thread = payload.thread ? new ObjectId(payload.thread) : false; let orTerms = payload.or || {}; @@ -87,6 +131,10 @@ const prepareSearchFilter = async (db, user, payload) => { filter.mailbox = { $nin: mailboxes.map(m => m._id) }; } + if (filter.mailbox && idQuery) { + filter.uid = idQuery; + } + if (thread) { filter.thread = thread; } @@ -267,4 +315,4 @@ const prepareSearchFilter = async (db, user, payload) => { return { filter, query }; }; -module.exports = prepareSearchFilter; +module.exports = { uidRangeStringToQuery, prepareSearchFilter }; diff --git a/lib/tasks/search-apply.js b/lib/tasks/search-apply.js index 5561ccaa..70dc5fc0 100644 --- a/lib/tasks/search-apply.js +++ b/lib/tasks/search-apply.js @@ -3,7 +3,7 @@ const log = require('npmlog'); const db = require('../db'); const util = require('util'); -const prepareSearchFilter = require('../prepare-search-filter'); +const { prepareSearchFilter } = require('../prepare-search-filter'); const { getMongoDBQuery } = require('../search-query'); const ObjectId = require('mongodb').ObjectId;