'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; };