exports.MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; exports.isNotEmpty = function(str) { return str.trim().length > 0; }; exports.escape = function(str) { return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); }; exports.unescape = function(str) { return str.replace(/\\"/g, '"').replace(/\\\\/g, '\\'); }; exports.buildSearchQuery = function(options, extensions, isOrChild) { var searchargs = ''; for (var i=0,len=options.length; i 1) args = criteria.slice(1); if (criteria.length > 0) criteria = criteria[0].toUpperCase(); } else throw new Error('Unexpected search option data type. ' + 'Expected string or array. Got: ' + typeof criteria); if (criteria === 'OR') { if (args.length !== 2) throw new Error('OR must have exactly two arguments'); searchargs += ' OR ('; searchargs += exports.buildSearchQuery(args[0], extensions, true); searchargs += ') ('; searchargs += exports.buildSearchQuery(args[1], extensions, true); searchargs += ')'; } else { if (criteria[0] === '!') { modifier += 'NOT '; criteria = criteria.substr(1); } switch(criteria) { // -- Standard criteria -- case 'ALL': case 'ANSWERED': case 'DELETED': case 'DRAFT': case 'FLAGGED': case 'NEW': case 'SEEN': case 'RECENT': case 'OLD': case 'UNANSWERED': case 'UNDELETED': case 'UNDRAFT': case 'UNFLAGGED': case 'UNSEEN': searchargs += modifier + criteria; break; case 'BCC': case 'BODY': case 'CC': case 'FROM': case 'SUBJECT': case 'TEXT': case 'TO': if (!args || args.length !== 1) throw new Error('Incorrect number of arguments for search option: ' + criteria); searchargs += modifier + criteria + ' "' + exports.escape(''+args[0]) + '"'; break; case 'BEFORE': case 'ON': case 'SENTBEFORE': case 'SENTON': case 'SENTSINCE': case 'SINCE': if (!args || args.length !== 1) throw new Error('Incorrect number of arguments for search option: ' + criteria); else if (!(args[0] instanceof Date)) { if ((args[0] = new Date(args[0])).toString() === 'Invalid Date') throw new Error('Search option argument must be a Date object' + ' or a parseable date string'); } searchargs += modifier + criteria + ' ' + args[0].getDate() + '-' + exports.MONTHS[args[0].getMonth()] + '-' + args[0].getFullYear(); break; case 'KEYWORD': case 'UNKEYWORD': if (!args || args.length !== 1) throw new Error('Incorrect number of arguments for search option: ' + criteria); searchargs += modifier + criteria + ' ' + args[0]; break; case 'LARGER': case 'SMALLER': if (!args || args.length !== 1) throw new Error('Incorrect number of arguments for search option: ' + criteria); var num = parseInt(args[0], 10); if (isNaN(num)) throw new Error('Search option argument must be a number'); searchargs += modifier + criteria + ' ' + args[0]; break; case 'HEADER': if (!args || args.length !== 2) throw new Error('Incorrect number of arguments for search option: ' + criteria); searchargs += modifier + criteria + ' "' + exports.escape(''+args[0]) + '" "' + exports.escape(''+args[1]) + '"'; break; case 'UID': if (!args) throw new Error('Incorrect number of arguments for search option: ' + criteria); exports.validateUIDList(args); searchargs += modifier + criteria + ' ' + args.join(','); break; // -- Extensions criteria -- case 'X-GM-MSGID': // Gmail unique message ID case 'X-GM-THRID': // Gmail thread ID if (extensions.indexOf('X-GM-EXT-1') === -1) throw new Error('IMAP extension not available: ' + criteria); var val; if (!args || args.length !== 1) throw new Error('Incorrect number of arguments for search option: ' + criteria); else { val = ''+args[0]; if (!(/^\d+$/.test(args[0]))) throw new Error('Invalid value'); } searchargs += modifier + criteria + ' ' + val; break; case 'X-GM-RAW': // Gmail search syntax if (extensions.indexOf('X-GM-EXT-1') === -1) throw new Error('IMAP extension not available: ' + criteria); if (!args || args.length !== 1) throw new Error('Incorrect number of arguments for search option: ' + criteria); searchargs += modifier + criteria + ' "' + exports.escape(''+args[0]) + '"'; break; case 'X-GM-LABELS': // Gmail labels if (extensions.indexOf('X-GM-EXT-1') === -1) throw new Error('IMAP extension not available: ' + criteria); if (!args || args.length !== 1) throw new Error('Incorrect number of arguments for search option: ' + criteria); searchargs += modifier + criteria + ' ' + args[0]; break; default: try { // last hope it's a seqno set // http://tools.ietf.org/html/rfc3501#section-6.4.4 var seqnos = (args ? [criteria].concat(args) : [criteria]); exports.validateUIDList(seqnos); searchargs += modifier + seqnos.join(','); } catch(e) { throw new Error('Unexpected search option: ' + criteria); } } } if (isOrChild) break; } return searchargs; }; exports.validateUIDList = function(uids) { for (var i=0,len=uids.length,intval; i 1) uids = ['*']; break; } else if (/^(?:[\d]+|\*):(?:[\d]+|\*)$/.test(uids[i])) continue; } intval = parseInt(''+uids[i], 10); if (isNaN(intval)) { throw new Error('Message ID/number must be an integer, "*", or a range: ' + uids[i]); } else if (typeof uids[i] !== 'number') uids[i] = intval; } }; var CHARR_CRLF = [13, 10]; function line(b, s) { var len = b.length, p = b.p, start = p, ret = false, retest = false; while (p < len && !ret) { if (b[p] === CHARR_CRLF[s.p]) { if (++s.p === 2) ret = true; } else { retest = (s.p > 0); s.p = 0; if (retest) continue; } ++p; } if (ret === false) { if (s.ret) s.ret += b.toString('ascii', start); else s.ret = b.toString('ascii', start); } else { var iCR = p - 2; if (iCR < 0) { // the CR is at the end of s.ret if (s.ret && s.ret.length > 1) ret = s.ret.substr(0, s.ret.length - 1); else ret = ''; } else { // the entire CRLF is in b if (iCR === 0) ret = (s.ret ? s.ret : ''); else { if (s.ret) { ret = s.ret; ret += b.toString('ascii', start, iCR); } else ret = b.toString('ascii', start, iCR); } } s.p = 0; s.ret = undefined; } b.p = p; return ret; } exports.line = line;