diff --git a/imap-core/lib/indexer/parse-mime-tree.js b/imap-core/lib/indexer/parse-mime-tree.js index 5661508..1fe767e 100644 --- a/imap-core/lib/indexer/parse-mime-tree.js +++ b/imap-core/lib/indexer/parse-mime-tree.js @@ -1,6 +1,7 @@ 'use strict'; const addressparser = require('nodemailer/lib/addressparser'); +const libmime = require('libmime'); /** * Parses a RFC822 message into a structured object (JSON compatible) @@ -254,61 +255,19 @@ class MIMEParser { * @return {Object} Parsed value */ parseValueParams(headerValue) { - let data = { - value: '', - type: '', - subtype: '', - params: {} + let parsed = libmime.parseHeaderValue(headerValue) || {}; + + let subtype = (parsed.value || '').split('/'); + let type = (subtype.shift() || '').toLowerCase(); + subtype = subtype.join('/'); + + return { + value: parsed.value || '', + type, + subtype, + params: parsed.params || {}, + hasParams: !!Object.keys(parsed.params || {}).length }; - let match; - let processEncodedWords = {}; - - (headerValue || '').split(';').forEach((part, i) => { - let key, value; - if (!i) { - data.value = part.trim(); - data.subtype = data.value.split('/'); - data.type = (data.subtype.shift() || '').toLowerCase(); - data.subtype = data.subtype.join('/'); - return; - } - value = part.split('='); - key = (value.shift() || '').trim().toLowerCase(); - value = value.join('=').replace(/^['"\s]*|['"\s]*$/g, ''); - - // Do not touch headers that have strange looking keys, keep these - // only in the unparsed array - if (/[^a-zA-Z0-9\-*]/.test(key) || key.length >= 100) { - return; - } - - // This regex allows for an optional trailing asterisk, for headers - // which are encoded with lang/charset info as well as a continuation. - // See https://tools.ietf.org/html/rfc2231 section 4.1. - if ((match = key.match(/^([^*]+)\*(\d)?\*?$/))) { - if (!processEncodedWords[match[1]]) { - processEncodedWords[match[1]] = []; - } - processEncodedWords[match[1]][Number(match[2]) || 0] = value; - } else { - data.params[key] = value; - } - data.hasParams = true; - }); - - // convert extended mime word into a regular one - Object.keys(processEncodedWords).forEach(key => { - let charset = ''; - let value = ''; - processEncodedWords[key].forEach(val => { - let parts = val.split("'"); // eslint-disable-line quotes - charset = charset || parts.shift(); - value += (parts.pop() || '').replace(/%/g, '='); - }); - data.params[key] = '=?' + (charset || 'ISO-8859-1').toUpperCase() + '?Q?' + value + '?='; - }); - - return data; } /** @@ -337,4 +296,6 @@ function parse(rfc822) { return response; } +parse.MIMEParser = MIMEParser; + module.exports = parse; diff --git a/imap-core/test/parse-mime-tree-test.js b/imap-core/test/parse-mime-tree-test.js new file mode 100644 index 0000000..44ef9cc --- /dev/null +++ b/imap-core/test/parse-mime-tree-test.js @@ -0,0 +1,33 @@ +/*eslint no-unused-expressions: 0, prefer-arrow-callback: 0 */ + +'use strict'; + +const MIMEParser = require('../lib/indexer/parse-mime-tree').MIMEParser; + +const chai = require('chai'); +const expect = chai.expect; +chai.config.includeStack = true; + +describe('#parseValueParams', function () { + it.only('should return as is', function () { + let parser = new MIMEParser(); + const parsed = parser.parseValueParams( + 'text/plain;\n' + + '\tname*0=emailengine_uuendamise_kasud_ja_muud_asjad_ja_veelgi_pikem_pealk;\n' + + '\tname*1=iri.txt;\n' + + '\tx-apple-part-url=99AFDE83-8953-43B4-BE59-F59D6160AFAB' + ); + + console.log('PARSED', parsed); + expect(parsed).to.equal({ + value: 'text/plain', + type: 'text', + subtype: 'plain', + params: { + 'x-apple-part-url': '99AFDE83-8953-43B4-BE59-F59D6160AFAB', + name: 'emailengine_uuendamise_kasud_ja_muud_asjad_ja_veelgi_pikem_pealkiri.txt' + }, + hasParams: true + }); + }); +}); diff --git a/lib/search-query.js b/lib/search-query.js index 69e62a3..fa7c1bd 100644 --- a/lib/search-query.js +++ b/lib/search-query.js @@ -314,8 +314,213 @@ const getMongoDBQuery = async (db, user, queryStr) => { return { user: false }; }; +/* +const getElasticSearchQuery = async (db, user, queryStr) => { + const parsed = parseSearchQuery(queryStr); -module.exports = { parseSearchQuery, getMongoDBQuery }; + let searchQuery = { + bool: { + must: [ + { + term: { + user: (user || '').toString().trim() + } + } + ] + } + }; + + let curNode = searchQuery; + + let walkTree = async (node, curNode) => { + 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 = { + bool: { must: [] } + }; + + for (let entry of node.$and) { + let subBranch = await walkTree(entry); + branch.bool.must = branch.bool.must.concat(subBranch || []); + } + + return branch; + } else if (node.$or && node.$or.length) { + let branch = { + bool: { should: [], minimum_should_match: 1 } + }; + + for (let entry of node.$or) { + let subBranch = await walkTree(entry); + + branch.bool.should = branch.bool.should.concat(subBranch || []); + } + + return branch; + } else if (node.text) { + let branch = { + bool: { + should: [ + { + match: { + 'text.plain': { + query: node.text.value, + operator: 'and' + } + } + }, + { + match: { + 'text.html': { + query: node.text.value, + operator: 'and' + } + } + } + ], + minimum_should_match: 1 + } + }; + + if (node.text.negated) { + // FIXME: negation support! + } + + 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) { + let filter = await walkTree(Array.isArray(parsed) ? { $and: parsed } : parsed); + + let extras = { user }; + if (hasTextFilter) { + extras.searchable = true; + } + + return Object.assign({ user: null }, filter, extras); + } + + return { user: false }; +}; +*/ + +module.exports = { parseSearchQuery, getMongoDBQuery /*, getElasticSearchQuery*/ }; /* const util = require('util');