wildduck/imap-core/lib/commands/search.js

282 lines
8.5 KiB
JavaScript
Raw Normal View History

2017-03-06 05:45:50 +08:00
'use strict';
let imapHandler = require('../handler/imap-handler');
let imapTools = require('../imap-tools');
module.exports = {
state: 'Selected',
schema: false, // recursive, can't predefine
handler(command, callback) {
// Check if SEARCH method is set
if (typeof this._server.onSearch !== 'function') {
return callback(null, {
response: 'NO',
message: command.command + ' not implemented'
});
}
let isUid = (command.command || '').toString().toUpperCase() === 'UID SEARCH' ? true : false;
let terms = [];
let getTerms = elements => {
elements.forEach(element => {
if (Array.isArray(element)) {
return getTerms(element);
}
terms.push(element.value);
});
};
getTerms([].concat(command.attributes || []));
let parsed;
try {
parsed = parseQueryTerms(terms, this.selected.uidList);
} catch (E) {
return callback(E);
}
// mark CONDSTORE as enabled
if (parsed.terms.indexOf('modseq') >= 0 && !this.selected.condstoreEnabled) {
this.condstoreEnabled = this.selected.condstoreEnabled = true;
}
this._server.onSearch(this.selected.mailbox, {
query: parsed.query,
terms: parsed.terms,
isUid
}, this.session, (err, results) => {
if (err) {
return callback(err);
}
let matches = results.uidList;
if (typeof matches === 'string') {
return callback(null, {
response: 'NO',
code: matches.toUpperCase()
});
}
let response = {
tag: '*',
command: 'SEARCH',
attributes: []
};
if (Array.isArray(matches) && matches.length) {
matches.sort((a, b) => (a - b));
matches.forEach(nr => {
let seq;
if (!isUid) {
seq = this.selected.uidList.indexOf(nr) + 1;
if (seq) {
response.attributes.push({
type: 'atom',
value: String(seq)
});
}
} else {
response.attributes.push({
type: 'atom',
value: String(nr)
});
}
});
}
// append (MODSEQ 123) for queries that include MODSEQ criteria
if (results.highestModseq && parsed.terms.indexOf('modseq') >= 0) {
response.attributes.push(
[{
type: 'atom',
value: 'MODSEQ'
}, {
type: 'atom',
value: String(results.highestModseq)
}]
);
}
this.send(imapHandler.compiler(response));
return callback(null, {
response: 'OK'
});
});
},
parseQueryTerms // expose for testing
};
function parseQueryTerms(terms, uidList) {
terms = [].concat(terms || []);
let pos = 0;
let term;
let returnTerms = [];
let parsed = {
terms: []
};
let getTerm = level => {
level = level || 0;
if (pos >= terms.length) {
return undefined; // eslint-disable-line no-undefined
}
let term = terms[pos++];
let termType = imapTools.searchSchema[term.toLowerCase()];
let termCount = termType && termType.length;
let curTerm = [term.toLowerCase()];
// MODSEQ is special case as it includes 2 optional arguments
// If the next argument is a number then there is only one argument,
// otherwise there is 3 arguments
if (curTerm[0] === 'modseq') {
termType = isNaN(terms[pos]) ? termType[0] : termType[1];
termCount = termType.length;
}
if (!termType) {
// try if it is a sequence set
if (imapTools.validateSequnce(term)) {
// resolve sequence list to an array of UID values
curTerm = ['uid', imapTools.getMessageRange(uidList, term, false)];
} else {
// no idea what the term is for
throw new Error('Unknown search term ' + term.toUpperCase());
}
} else if (termCount) {
for (let i = 0, len = termCount; i < len; i++) {
if (termType[i] === 'expression') {
curTerm.push(getTerm(level + 1));
} else if (termType[i] === 'sequence') {
if (!imapTools.validateSequnce(terms[pos])) {
throw new Error('Invalid sequence set for ' + term.toUpperCase());
}
// resolve sequence list to an array of UID values
curTerm.push(imapTools.getMessageRange(uidList, terms[pos++], true));
} else {
curTerm.push(terms[pos++]);
}
}
}
if (imapTools.searchMapping.hasOwnProperty(curTerm[0])) {
curTerm = normalizeTerm(curTerm, imapTools.searchMapping[curTerm[0]]);
}
// return multiple values at once, should be already formatted
if (typeof curTerm[0] === 'object') {
return curTerm;
}
// keep a list of used terms
if (parsed.terms.indexOf(curTerm[0]) < 0) {
parsed.terms.push(curTerm[0]);
}
let response = {
key: curTerm[0]
};
switch (response.key) {
case 'not':
// make sure not is not an array, instead return several 'not' expressions
response = [].concat(curTerm[1] || []).map(val => ({
key: 'not',
value: val
}));
if (response.length === 1) {
response = response[0];
}
break;
case 'or':
// ensure that value is alwas an array
response.value = [].concat(curTerm.slice(1) || []);
break;
case 'header':
response.header = (curTerm[1] || '').toString().toLowerCase();
response.value = (curTerm[2] || '').toString(); // empty header value means that the header key must be present
break;
case 'date':
case 'internaldate':
response.operator = curTerm[1];
response.value = (curTerm[2] || '').toString();
break;
case 'size':
if (isNaN(curTerm[2])) {
throw new Error('Invalid size argument for ' + response.key);
}
response.operator = curTerm[1];
response.value = Number(curTerm[2]) || 0;
break;
case 'modseq':
if (isNaN(curTerm[curTerm.length - 1])) {
throw new Error('Invalid MODSEQ argument');
}
response.value = Number(curTerm[curTerm.length - 1]) || 0;
break;
default:
if (curTerm.length) {
response.value = curTerm.length > 2 ? curTerm.slice(1) : curTerm[1];
}
}
return response;
};
while (typeof (term = getTerm()) !== 'undefined') {
if (Array.isArray(term)) {
// flatten arrays
returnTerms = returnTerms.concat(term);
} else {
returnTerms.push(term);
}
}
parsed.terms.sort();
parsed.query = returnTerms;
return parsed;
}
function normalizeTerm(term, mapping) {
let flags;
let result = [mapping.key].concat(mapping.value.map(val => (val === '$1' ? term[1] : val)));
if (result[0] === 'flag') {
flags = [];
result.forEach((val, i) => {
if (i && (i % 2 !== 0)) {
flags.push({
key: 'flag',
value: val,
exists: !!result[i + 1]
});
}
});
return flags;
}
return result;
}