wildduck/imap-core/lib/search.js

331 lines
9.6 KiB
JavaScript
Raw Normal View History

2017-03-06 05:45:50 +08:00
'use strict';
const Indexer = require('./indexer/indexer');
2017-06-03 14:51:58 +08:00
const indexer = new Indexer();
2017-03-06 05:45:50 +08:00
module.exports.matchSearchQuery = matchSearchQuery;
2017-06-03 14:51:58 +08:00
const queryHandlers = {
2017-03-06 05:45:50 +08:00
// always matches
2017-03-10 22:59:04 +08:00
all(message, query, callback) {
return callback(null, true);
2017-03-06 05:45:50 +08:00
},
// matches if the message object includes (exists:true) or does not include (exists:false) specifiec flag
2017-03-10 22:59:04 +08:00
flag(message, query, callback) {
2017-03-06 05:45:50 +08:00
let pos = [].concat(message.flags || []).indexOf(query.value);
2017-03-10 22:59:04 +08:00
return callback(null, query.exists ? pos >= 0 : pos < 0);
2017-03-06 05:45:50 +08:00
},
// matches message receive date
2017-03-10 22:59:04 +08:00
internaldate(message, query, callback) {
2017-03-06 05:45:50 +08:00
switch (query.operator) {
case '<':
2017-04-13 16:35:39 +08:00
return callback(null, getShortDate(message.idate) < getShortDate(query.value));
2017-03-06 05:45:50 +08:00
case '=':
2017-04-13 16:35:39 +08:00
return callback(null, getShortDate(message.idate) === getShortDate(query.value));
2017-03-06 05:45:50 +08:00
case '>=':
2017-04-13 16:35:39 +08:00
return callback(null, getShortDate(message.idate) >= getShortDate(query.value));
2017-03-06 05:45:50 +08:00
}
2017-03-10 22:59:04 +08:00
return callback(null, false);
2017-03-06 05:45:50 +08:00
},
// matches message header date
2017-03-10 22:59:04 +08:00
date(message, query, callback) {
2017-03-10 21:20:13 +08:00
let date;
2017-04-10 22:12:47 +08:00
if (message.hdate) {
date = message.hdate;
2017-03-10 21:20:13 +08:00
} else {
let mimeTree = message.mimeTree;
if (!mimeTree) {
mimeTree = indexer.parseMimeTree(message.raw);
}
2017-04-13 16:35:39 +08:00
date = mimeTree.parsedHeader.date || message.idate;
2017-03-06 05:45:50 +08:00
}
switch (query.operator) {
case '<':
2017-03-10 22:59:04 +08:00
return callback(null, getShortDate(date) < getShortDate(query.value));
2017-03-06 05:45:50 +08:00
case '=':
2017-03-10 22:59:04 +08:00
return callback(null, getShortDate(date) === getShortDate(query.value));
2017-03-06 05:45:50 +08:00
case '>=':
2017-03-10 22:59:04 +08:00
return callback(null, getShortDate(date) >= getShortDate(query.value));
2017-03-06 05:45:50 +08:00
}
2017-03-10 22:59:04 +08:00
return callback(null, false);
2017-03-06 05:45:50 +08:00
},
// matches message body
2017-03-10 22:59:04 +08:00
body(message, query, callback) {
2017-06-03 14:51:58 +08:00
let data = indexer.getContents(
message.mimeTree,
{
type: 'text'
},
2019-07-18 16:01:29 +08:00
{ skipExternal: true }
2017-06-03 14:51:58 +08:00
);
2017-03-10 22:59:04 +08:00
let resolveData = next => {
if (data.type !== 'stream') {
return next(null, data.value);
}
let chunks = [];
let chunklen = 0;
data.value.once('error', err => next(err));
data.value.on('readable', () => {
let chunk;
while ((chunk = data.value.read()) !== null) {
chunks.push(chunk);
chunklen += chunk.length;
}
});
data.value.on('end', () => {
next(null, Buffer.concat(chunks, chunklen));
});
};
resolveData((err, body) => {
if (err) {
return callback(err);
}
2019-07-18 16:01:29 +08:00
callback(
null,
body
.toString()
.toLowerCase()
.indexOf((query.value || '').toString().toLowerCase()) >= 0
);
2017-03-10 22:59:04 +08:00
});
2017-03-06 05:45:50 +08:00
},
// matches message source
2017-03-10 22:59:04 +08:00
text(message, query, callback) {
2017-06-03 14:51:58 +08:00
let data = indexer.getContents(
message.mimeTree,
{
type: 'content'
},
2019-07-18 16:01:29 +08:00
{ skipExternal: true }
2017-06-03 14:51:58 +08:00
);
2017-03-10 22:59:04 +08:00
let resolveData = next => {
if (data.type !== 'stream') {
return next(null, data.value);
}
let chunks = [];
let chunklen = 0;
data.value.once('error', err => next(err));
data.value.on('readable', () => {
let chunk;
while ((chunk = data.value.read()) !== null) {
chunks.push(chunk);
chunklen += chunk.length;
}
});
data.value.on('end', () => {
next(null, Buffer.concat(chunks, chunklen));
});
};
resolveData((err, text) => {
if (err) {
return callback(err);
}
2019-07-18 16:01:29 +08:00
callback(
null,
text
.toString()
.toLowerCase()
.indexOf((query.value || '').toString().toLowerCase()) >= 0
);
2017-03-10 22:59:04 +08:00
});
2017-03-06 05:45:50 +08:00
},
// matches message UID number. Sequence queries are also converted to UID queries
2017-03-10 22:59:04 +08:00
uid(message, query, callback) {
return callback(null, query.value.indexOf(message.uid) >= 0);
2017-03-06 05:45:50 +08:00
},
// matches message source size
2017-03-10 22:59:04 +08:00
size(message, query, callback) {
2017-03-10 21:20:13 +08:00
let size = message.size;
if (!size) {
size = (message.raw || '').length;
}
2017-03-06 05:45:50 +08:00
switch (query.operator) {
case '<':
2017-03-10 22:59:04 +08:00
return callback(null, size < query.value);
2017-03-06 05:45:50 +08:00
case '=':
2017-03-10 22:59:04 +08:00
return callback(null, size === query.value);
2017-03-06 05:45:50 +08:00
case '>':
2017-03-10 22:59:04 +08:00
return callback(null, size > query.value);
2017-03-06 05:45:50 +08:00
}
2017-03-10 22:59:04 +08:00
return callback(null, false);
2017-03-06 05:45:50 +08:00
},
// matches message headers
2017-03-10 22:59:04 +08:00
header(message, query, callback) {
2017-03-06 05:45:50 +08:00
let mimeTree = message.mimeTree;
if (!mimeTree) {
mimeTree = indexer.parseMimeTree(message.raw || '');
}
2017-06-03 14:51:58 +08:00
let headers = mimeTree.header || [];
2017-03-06 05:45:50 +08:00
let header = query.header;
let term = (query.value || '').toString().toLowerCase();
let key, value, parts;
for (let i = 0, len = headers.length; i < len; i++) {
parts = headers[i].split(':');
key = (parts.shift() || '').trim().toLowerCase();
2017-06-03 14:51:58 +08:00
value = parts.join(':') || '';
2017-03-06 05:45:50 +08:00
if (key === header && (!term || value.toLowerCase().indexOf(term) >= 0)) {
2017-03-10 22:59:04 +08:00
return callback(null, true);
2017-03-06 05:45:50 +08:00
}
}
2017-03-10 22:59:04 +08:00
return callback(null, false);
2017-03-06 05:45:50 +08:00
},
// matches messages with modifyIndex exual or greater than criteria
2017-03-10 22:59:04 +08:00
modseq(message, query, callback) {
return callback(null, message.modseq >= query.value);
2017-03-06 05:45:50 +08:00
},
// charset argument is ignored
2017-03-10 22:59:04 +08:00
charset(message, query, callback) {
return callback(null, true);
2017-03-06 05:45:50 +08:00
}
};
/**
* Returns a date object with time set to 00:00 on UTC timezone
*
* @param {String|Date} date Date to convert
* @returns {Date} Date object without time
*/
function getShortDate(date) {
date = date || new Date();
if (typeof date === 'string' || typeof date === 'number') {
date = new Date(date);
}
return date.toISOString().substr(0, 10);
}
/**
* Checks if a specific search term match the message or not
*
* @param {Object} message Stored message object
* @param {Object} query Query term object
* @returns {Boolean} Term matched (true) or not (false)
*/
2017-03-10 22:59:04 +08:00
function matchSearchTerm(message, query, callback) {
2017-03-06 05:45:50 +08:00
if (Array.isArray(query)) {
// AND, all terms need to match
2017-03-10 22:59:04 +08:00
return matchSearchQuery(message, query, callback);
2017-03-06 05:45:50 +08:00
}
if (!query || typeof query !== 'object') {
// unknown query term
2017-03-10 22:59:04 +08:00
return setImmediate(() => callback(null, false));
2017-03-06 05:45:50 +08:00
}
switch (query.key) {
2017-06-03 14:51:58 +08:00
case 'or': {
// OR, only single match needed
let checked = 0;
let checkNext = () => {
if (checked >= query.value.length) {
return callback(null, false);
}
let term = query.value[checked++];
matchSearchTerm(message, term, (err, match) => {
if (err) {
return callback(err);
2017-03-10 22:59:04 +08:00
}
2017-06-03 14:51:58 +08:00
if (match) {
return callback(null, true);
}
setImmediate(checkNext);
});
};
return setImmediate(checkNext);
}
/*
2017-03-06 05:45:50 +08:00
// OR, only single match needed
for (let i = query.value.length - 1; i >= 0; i--) {
if (matchSearchTerm(message, query.value[i])) {
return true;
}
}
return false;
2017-03-10 22:59:04 +08:00
*/
2017-03-06 05:45:50 +08:00
case 'not':
// return reverse match
2017-03-10 22:59:04 +08:00
return matchSearchTerm(message, query.value, (err, match) => {
if (err) {
return callback(err);
}
callback(null, !match);
});
2017-03-06 05:45:50 +08:00
default:
// check if there is a handler for the term and use it
if (queryHandlers.hasOwnProperty(query.key)) {
2017-03-10 22:59:04 +08:00
return setImmediate(() => queryHandlers[query.key](message, query, callback));
2017-03-06 05:45:50 +08:00
}
2017-03-10 22:59:04 +08:00
return setImmediate(() => callback(null, false));
2017-03-06 05:45:50 +08:00
}
}
/**
* Traverses query tree and checks if all query terms match or not. Stops on first false match occurence
*
* @param {Object} message Stored message object
* @param {Object} query Query term object
* @returns {Boolean} Term matched (true) or not (false)
*/
2017-03-10 22:59:04 +08:00
function matchSearchQuery(message, query, callback) {
2017-03-06 05:45:50 +08:00
if (!Array.isArray(query)) {
query = [].concat(query || []);
}
2017-03-10 22:59:04 +08:00
let checked = 0;
let checkNext = () => {
if (checked >= query.length) {
return callback(null, true);
}
let term = query[checked++];
matchSearchTerm(message, term, (err, match) => {
if (err) {
return callback(err);
}
if (!match) {
return callback(null, false);
}
setImmediate(checkNext);
});
};
return setImmediate(checkNext);
/*
for (let i = 0, len = query.length; i < len; i++) {
if (!matchSearchTerm(message, query[i])) {
return false;
}
2017-03-06 05:45:50 +08:00
}
2017-03-10 22:59:04 +08:00
return true;
*/
2017-03-06 05:45:50 +08:00
}