NeahOpti/node_modules/imapflow/lib/search-compiler.js
2025-04-22 12:49:37 +02:00

270 lines
9.1 KiB
JavaScript

'use strict';
const { formatDate, formatFlag, canUseFlag } = require('./tools.js');
let setBoolOpt = (attributes, term, value) => {
if (!value) {
if (/^un/i.test(term)) {
term = term.slice(2);
} else {
term = 'UN' + term;
}
}
attributes.push({ type: 'ATOM', value: term.toUpperCase() });
};
let setOpt = (attributes, term, value, type) => {
type = type || 'ATOM';
if (value === false || value === null) {
attributes.push({ type, value: 'NOT' });
}
attributes.push({ type, value: term.toUpperCase() });
if (Array.isArray(value)) {
value.forEach(entry => attributes.push({ type, value: (entry || '').toString() }));
} else {
attributes.push({ type, value: value.toString() });
}
};
let processDateField = (attributes, term, value) => {
let date = formatDate(value);
if (!date) {
return;
}
setOpt(attributes, term, date);
};
let isUnicodeString = str => {
if (!str || typeof str !== 'string') {
return false;
}
return Buffer.byteLength(str) !== str.length;
};
module.exports.searchCompiler = (connection, query) => {
const attributes = [];
let hasUnicode = false;
const mailbox = connection.mailbox;
const walk = params => {
Object.keys(params || {}).forEach(term => {
switch (term.toUpperCase()) {
case 'SEQ': // custom key for sequence range
{
let value = params[term];
if (typeof value === 'number') {
value = value.toString();
}
if (typeof value === 'string' && /^\S+$/.test(value)) {
attributes.push({ type: 'SEQUENCE', value });
}
}
break;
case 'ANSWERED':
case 'DELETED':
case 'DRAFT':
case 'FLAGGED':
case 'SEEN':
case 'UNANSWERED':
case 'UNDELETED':
case 'UNDRAFT':
case 'UNFLAGGED':
case 'UNSEEN':
// toggles UN-prefix for falsy values
setBoolOpt(attributes, term, !!params[term]);
break;
case 'ALL':
case 'NEW':
case 'OLD':
case 'RECENT':
if (params[term]) {
setBoolOpt(attributes, term, true);
}
break;
case 'LARGER':
case 'SMALLER':
case 'MODSEQ':
if (params[term]) {
setOpt(attributes, term, params[term]);
}
break;
case 'BCC':
case 'BODY':
case 'CC':
case 'FROM':
case 'SUBJECT':
case 'TEXT':
case 'TO':
if (isUnicodeString(params[term])) {
hasUnicode = true;
}
if (params[term]) {
setOpt(attributes, term, params[term]);
}
break;
case 'UID':
if (params[term]) {
setOpt(attributes, term, params[term], 'SEQUENCE');
}
break;
case 'EMAILID':
if (connection.capabilities.has('OBJECTID')) {
setOpt(attributes, 'EMAILID', params[term]);
} else if (connection.capabilities.has('X-GM-EXT-1')) {
setOpt(attributes, 'X-GM-MSGID', params[term]);
}
break;
case 'THREADID':
if (connection.capabilities.has('OBJECTID')) {
setOpt(attributes, 'THREADID', params[term]);
} else if (connection.capabilities.has('X-GM-EXT-1')) {
setOpt(attributes, 'X-GM-THRID', params[term]);
}
break;
case 'GMRAW':
case 'GMAILRAW': // alias for GMRAW
if (connection.capabilities.has('X-GM-EXT-1')) {
if (isUnicodeString(params[term])) {
hasUnicode = true;
}
setOpt(attributes, 'X-GM-RAW', params[term]);
} else {
let error = new Error('Server does not support X-GM-EXT-1 extension required for X-GM-RAW');
error.code = 'MissingServerExtension';
throw error;
}
break;
case 'BEFORE':
case 'ON':
case 'SINCE':
case 'SENTBEFORE':
case 'SENTON':
case 'SENTSINCE':
processDateField(attributes, term, params[term]);
break;
case 'KEYWORD':
case 'UNKEYWORD':
{
let flag = formatFlag(params[term]);
if (canUseFlag(mailbox, flag) || mailbox.flags.has(flag)) {
setOpt(attributes, term, flag);
}
}
break;
case 'HEADER':
if (params[term] && typeof params[term] === 'object') {
Object.keys(params[term]).forEach(header => {
let value = params[term][header];
if (value === true) {
value = '';
}
if (typeof value !== 'string') {
return;
}
if (isUnicodeString(value)) {
hasUnicode = true;
}
setOpt(attributes, term, [header.toUpperCase().trim(), value]);
});
}
break;
case 'OR':
{
if (!params[term] || !Array.isArray(params[term]) || !params[term].length) {
break;
}
if (params[term].length === 1) {
if (typeof params[term][0] === 'object' && params[term][0]) {
walk(params[term][0]);
}
break;
}
// OR values has to be grouped by 2
// OR conditional1 conditional2
let genOrTree = list => {
let group = false;
let groups = [];
list.forEach((entry, i) => {
if (i % 2 === 0) {
group = [entry];
} else {
group.push(entry);
groups.push(group);
group = false;
}
});
if (group && group.length) {
while (group.length === 1 && Array.isArray(group[0])) {
group = group[0];
}
groups.push(group);
}
while (groups.length > 2) {
groups = genOrTree(groups);
}
while (groups.length === 1 && Array.isArray(groups[0])) {
groups = groups[0];
}
return groups;
};
let walkOrTree = entry => {
if (Array.isArray(entry)) {
if (entry.length > 1) {
attributes.push({ type: 'ATOM', value: 'OR' });
}
entry.forEach(walkOrTree);
return;
}
if (entry && typeof entry === 'object') {
walk(entry);
}
};
walkOrTree(genOrTree(params[term]));
}
break;
}
});
};
walk(query);
if (hasUnicode && !connection.enabled.has('UTF8=ACCEPT')) {
// Prepend search query with `CHARSET UTF-8`
attributes.unshift({ type: 'ATOM', value: 'UTF-8' });
attributes.unshift({ type: 'ATOM', value: 'CHARSET' });
}
return attributes;
};