From 3ad81e4fa9655d54fc13c811f19ad7fa0f1960e0 Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Tue, 4 Apr 2017 10:20:25 +0300 Subject: [PATCH] Fixed SEARCH --- README.md | 6 +++--- imap.js | 54 ++++++++++++++++++++++++++++++++++------------------ indexes.json | 12 ++++++------ 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 6b31ee03..f0b10bc0 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,6 @@ Wild Duck IMAP server supports the following IMAP standards: - **UTF8=ACCEPT** (RFC6855) – this also means that Wild Duck natively supports unicode email usernames. For example <андрис@уайлддак.орг> is a valid email address that is hosted by a test instance of Wild Duck - **QUOTA** (RFC2087) – Quota size is global for an account, using a single quota root. Be aware that quota size does not mean actual byte storage in disk, it is calculated as the sum of the rfc822 sources of stored messages. Actual disk usage is larger as there are database overhead per every message. -> **NB** The master branch of Wild Duck does not perform SEARCH TEXT and SEARCH BODY right now. Use a tagged version to get fully functional SEARCH - Wild Duck more or less passes the [ImapTest](https://www.imapwiki.org/ImapTest/TestFeatures). Common errors that arise in the test are unknown labels (Wild Duck doesn't send unsolicited FLAGS updates) and NO for STORE (messages deleted in one session can not be updated in another). ## FAQ @@ -137,6 +135,7 @@ TODO: 1. Expose counters (seen/unseen messages, message count in mailbox etc.) 2. Search messages +3. Expose journal updates through WebSocket or similar ### POST /user/create @@ -580,7 +579,8 @@ This is a list of known differences from the IMAP specification. Listed differen 4. Wild Duck responds with `NO` for `STORE` if matching messages were deleted in another session 5. `CHARSET` argument for the `SEARCH` command is ignored (RFC3501 6.4.4.) 6. Metadata arguments for `SEARCH MODSEQ` are ignored (RFC7162 3.1.5.). You can define `` and `` values but these are not used for anything -7. What happens when FETCH is called for messages that were deleted in another session? (_Not sure, need to check_) +7. `SEARCH TEXT` and `SEARCH BODY` both use MongoDB [$text index](https://docs.mongodb.com/v3.4/reference/operator/query/text/) against decoded plaintext version of the message. RFC3501 assumes that it should be a string match either against full message (`TEXT`) or body section (`BODY`). +8. What happens when FETCH is called for messages that were deleted in another session? (_Not sure, need to check_) Any other differences are most probably real bugs and unintentional. diff --git a/imap.js b/imap.js index b73d09e5..0d4b3d43 100644 --- a/imap.js +++ b/imap.js @@ -1179,8 +1179,9 @@ server.onSearch = function (path, options, session, callback) { //if (!options.terms.includes('all')) { let hasAll = false; + let nothing = false; let walkQuery = (parent, ne, node) => { - if (hasAll) { + if (hasAll || nothing) { return; } node.forEach(term => { @@ -1212,18 +1213,19 @@ server.onSearch = function (path, options, session, callback) { break; } - case 'text': - // TODO: search over full content - parent.push({ - size: -1 - }); - break; - - case 'body': - // TODO: search over body text - parent.push({ - size: -1 - }); + case 'text': // search over entire email + case 'body': // search over email body + if (term.value && !ne) { + parent.push({ + // fulltext can not be in $not section + $text: { + $search: term.value + } + }); + } else { + // can not search by text + nothing = true; + } break; case 'modseq': @@ -1280,12 +1282,18 @@ server.onSearch = function (path, options, session, callback) { // FIXME: this does not match unicode symbols for whatever reason let regex = Buffer.from(term.value, 'binary').toString().replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); let entry = term.value ? { - 'headers.key': term.header, - 'headers.value': !ne ? { - regex - } : { - $not: { - regex + headers: { + $elemMatch: { + key: term.header, + value: !ne ? { + $regex: regex, + $options: 'i' + } : { + $not: { + $regex: regex, + $options: 'i' + } + } } } } : { @@ -1410,6 +1418,14 @@ server.onSearch = function (path, options, session, callback) { this.logger.info('SEARCH %s', JSON.stringify(query)); + if (nothing) { + // reject immediatelly + return callback(null, { + uidList: [], + highestModseq: 0 + }); + } + let cursor = db.database.collection('messages').find(query). project(projection). sort([ diff --git a/indexes.json b/indexes.json index 1006b2b9..c1d0dbc1 100644 --- a/indexes.json +++ b/indexes.json @@ -84,12 +84,6 @@ "mailbox": 1, "modseq": 1 } - }, { - "name": "by_flags", - "key": { - "mailbox": 1, - "flags": 1 - } }, { "name": "by_internaldate", "key": { @@ -121,6 +115,12 @@ "mailbox": 1, "hasAttachments": 1 } + }, { + "name": "fulltext", + "key": { + "mailbox": 1, + "text": "text" + } }] }, { "collection": "attachment.files",