mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-01-09 01:18:44 +08:00
281 lines
8.5 KiB
JavaScript
281 lines
8.5 KiB
JavaScript
'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;
|
|
}
|