var EventEmitter = require('events').EventEmitter, ReadableStream = require('stream').Readable || require('readable-stream').Readable, inherits = require('util').inherits, inspect = require('util').inspect; var utf7 = require('utf7').imap, jsencoding; // lazy-loaded var CH_LF = 10, LITPLACEHOLDER = String.fromCharCode(0), EMPTY_READCB = function(n) {}, RE_INTEGER = /^\d+$/, RE_PRECEDING = /^(?:\* |A\d+ |\+ ?)/, RE_BODYLITERAL = /BODY\[(.*)\] \{(\d+)\}$/i, RE_BODYINLINEKEY = /^BODY\[(.*)\]$/i, RE_SEQNO = /^\* (\d+)/, RE_LISTCONTENT = /^\((.*)\)$/, RE_LITERAL = /\{(\d+)\}$/, RE_UNTAGGED = /^\* (?:(OK|NO|BAD|BYE|FLAGS|ID|LIST|XLIST|LSUB|SEARCH|STATUS|CAPABILITY|NAMESPACE|PREAUTH|SORT|THREAD|ESEARCH|QUOTA|QUOTAROOT)|(\d+) (EXPUNGE|FETCH|RECENT|EXISTS))(?:(?: \[([^\]]+)\])?(?: (.+))?)?$/i, RE_TAGGED = /^A(\d+) (OK|NO|BAD) ?(?:\[([^\]]+)\] )?(.*)$/i, RE_CONTINUE = /^\+(?: (?:\[([^\]]+)\] )?(.+))?$/i, RE_CRLF = /\r\n/g, RE_HDR = /^([^:]+):[ \t]?(.+)?$/, RE_ENCWORD = /=\?([^?*]*?)(?:\*.*?)?\?([qb])\?(.*?)\?=/gi, RE_ENCWORD_END = /=\?([^?*]*?)(?:\*.*?)?\?([qb])\?(.*?)\?=$/i, RE_ENCWORD_BEGIN = /^[ \t]=\?([^?*]*?)(?:\*.*?)?\?([qb])\?(.*?)\?=/i, RE_QENC = /(?:=([a-fA-F0-9]{2}))|_/g, RE_SEARCH_MODSEQ = /^(.+) \(MODSEQ (.+?)\)$/i, RE_LWS_ONLY = /^[ \t]*$/; function Parser(stream, debug) { if (!(this instanceof Parser)) return new Parser(stream, debug); EventEmitter.call(this); this._stream = undefined; this._body = undefined; this._literallen = 0; this._literals = []; this._buffer = ''; this._ignoreReadable = false; this.debug = debug; var self = this; this._cbReadable = function() { if (self._ignoreReadable) return; if (self._literallen > 0 && !self._body) self._tryread(self._literallen); else self._tryread(); }; this.setStream(stream); process.nextTick(this._cbReadable); } inherits(Parser, EventEmitter); Parser.prototype.setStream = function(stream) { if (this._stream) this._stream.removeListener('readable', this._cbReadable); if (/^v0\.8\./.test(process.version)) { this._stream = (new ReadableStream()).wrap(stream); // since Readable.wrap() proxies events, we need to remove at least the // proxied 'error' event since this can cause problems and Parser doesn't // care about such events stream._events.error.pop(); } else this._stream = stream; this._stream.on('readable', this._cbReadable); }; Parser.prototype._tryread = function(n) { if (this._stream.readable) { var r = this._stream.read(n); r && this._parse(r); } }; Parser.prototype._parse = function(data) { var i = 0, datalen = data.length, idxlf; if (this._literallen > 0) { if (this._body) { var body = this._body; if (datalen >= this._literallen) { var litlen = this._literallen; i = litlen; this._literallen = 0; this._body = undefined; body._read = EMPTY_READCB; if (datalen > litlen) body.push(data.slice(0, litlen)); else body.push(data); body.push(null); } else { this._literallen -= datalen; var r = body.push(data); if (!r) { body._read = this._cbReadable; return; } i = datalen; } } else { if (datalen > this._literallen) this._literals.push(data.slice(0, this._literallen)); else this._literals.push(data); i = this._literallen; this._literallen = 0; } } while (i < datalen) { idxlf = indexOfCh(data, datalen, i, CH_LF); if (idxlf === -1) { this._buffer += data.toString('utf8', i); break; } else { this._buffer += data.toString('utf8', i, idxlf); this._buffer = this._buffer.trim(); i = idxlf + 1; this.debug && this.debug('<= ' + inspect(this._buffer)); if (RE_PRECEDING.test(this._buffer)) { var firstChar = this._buffer[0]; if (firstChar === '*') this._resUntagged(); else if (firstChar === 'A') this._resTagged(); else if (firstChar === '+') this._resContinue(); if (this._literallen > 0 && i < datalen) { this._ignoreReadable = true; // literal data included in this chunk -- put it back onto stream this._stream.unshift(data.slice(i)); this._ignoreReadable = false; i = datalen; if (!this._body) { // check if unshifted contents satisfies non-body literal length this._tryread(this._literallen); } } } else { this.emit('other', this._buffer); this._buffer = ''; } } } if (this._literallen === 0 || this._body) this._tryread(); }; Parser.prototype._resTagged = function() { var m; if (m = RE_LITERAL.exec(this._buffer)) { // non-BODY literal -- buffer it this._buffer = this._buffer.replace(RE_LITERAL, LITPLACEHOLDER); this._literallen = parseInt(m[1], 10); } else if (m = RE_TAGGED.exec(this._buffer)) { this._buffer = ''; this._literals = []; this.emit('tagged', { type: m[2].toLowerCase(), tagnum: parseInt(m[1], 10), textCode: (m[3] ? parseTextCode(m[3], this._literals) : m[3]), text: m[4] }); } else this._buffer = ''; }; Parser.prototype._resUntagged = function() { var m; if (m = RE_BODYLITERAL.exec(this._buffer)) { // BODY literal -- stream it var which = m[1], size = parseInt(m[2], 10); this._literallen = size; this._body = new ReadableStream(); this._body._readableState.sync = false; this._body._read = EMPTY_READCB; m = RE_SEQNO.exec(this._buffer); this._buffer = this._buffer.replace(RE_BODYLITERAL, ''); this.emit('body', this._body, { seqno: parseInt(m[1], 10), which: which, size: size }); } else if (m = RE_LITERAL.exec(this._buffer)) { // non-BODY literal -- buffer it this._buffer = this._buffer.replace(RE_LITERAL, LITPLACEHOLDER); this._literallen = parseInt(m[1], 10); } else if (m = RE_UNTAGGED.exec(this._buffer)) { this._buffer = ''; // normal single line response // m[1] or m[3] = response type // if m[3] is set, m[2] = sequence number (for FETCH) or count // m[4] = response text code (optional) // m[5] = response text (optional) var type, num, textCode, val; if (m[2] !== undefined) num = parseInt(m[2], 10); if (m[4] !== undefined) textCode = parseTextCode(m[4], this._literals); type = (m[1] || m[3]).toLowerCase(); if (type === 'flags' || type === 'search' || type === 'capability' || type === 'sort') { if (m[5]) { if (type === 'search' && RE_SEARCH_MODSEQ.test(m[5])) { // CONDSTORE search response var p = RE_SEARCH_MODSEQ.exec(m[5]); val = { results: p[1].split(' '), modseq: p[2] }; } else { if (m[5][0] === '(') val = RE_LISTCONTENT.exec(m[5])[1].split(' '); else val = m[5].split(' '); if (type === 'search' || type === 'sort') val = val.map(function(v) { return parseInt(v, 10); }); } } else val = []; } else if (type === 'thread') { if (m[5]) val = parseExpr(m[5], this._literals); else val = []; } else if (type === 'list' || type === 'lsub' || type === 'xlist') val = parseBoxList(m[5], this._literals); else if (type === 'id') val = parseId(m[5], this._literals); else if (type === 'status') val = parseStatus(m[5], this._literals); else if (type === 'fetch') val = parseFetch.call(this, m[5], this._literals, num); else if (type === 'namespace') val = parseNamespaces(m[5], this._literals); else if (type === 'esearch') val = parseESearch(m[5], this._literals); else if (type === 'quota') val = parseQuota(m[5], this._literals); else if (type === 'quotaroot') val = parseQuotaRoot(m[5], this._literals); else val = m[5]; this._literals = []; this.emit('untagged', { type: type, num: num, textCode: textCode, text: val }); } else this._buffer = ''; }; Parser.prototype._resContinue = function() { var m = RE_CONTINUE.exec(this._buffer), textCode, text; this._buffer = ''; if (!m) return; text = m[2]; if (m[1] !== undefined) textCode = parseTextCode(m[1], this._literals); this.emit('continue', { textCode: textCode, text: text }); }; function indexOfCh(buffer, len, i, ch) { var r = -1; for (; i < len; ++i) { if (buffer[i] === ch) { r = i; break; } } return r; } function parseTextCode(text, literals) { var r = parseExpr(text, literals); if (r.length === 1) return r[0]; else return { key: r[0], val: r.length === 2 ? r[1] : r.slice(1) }; } function parseESearch(text, literals) { var r = parseExpr(text.toUpperCase().replace('UID', ''), literals), attrs = {}; // RFC4731 unfortunately is lacking on documentation, so we're going to // assume that the response text always begins with (TAG "A123") and skip that // part ... for (var i = 1, len = r.length, key, val; i < len; i += 2) { key = r[i].toLowerCase(); val = r[i + 1]; if (key === 'all') val = val.toString().split(','); attrs[key] = val; } return attrs; } function parseId(text, literals) { var r = parseExpr(text, literals), id = {}; if (r[0] === null) return null; for (var i = 0, len = r[0].length; i < len; i += 2) id[r[0][i].toLowerCase()] = r[0][i + 1]; return id; } function parseQuota(text, literals) { var r = parseExpr(text, literals), resources = {}; for (var i = 0, len = r[1].length; i < len; i += 3) { resources[r[1][i].toLowerCase()] = { usage: r[1][i + 1], limit: r[1][i + 2] }; } return { root: r[0], resources: resources }; } function parseQuotaRoot(text, literals) { var r = parseExpr(text, literals); return { roots: r.slice(1), mailbox: r[0] }; } function parseBoxList(text, literals) { var r = parseExpr(text, literals); return { flags: r[0], delimiter: r[1], name: utf7.decode(''+r[2]) }; } function parseNamespaces(text, literals) { var r = parseExpr(text, literals), i, len, j, len2, ns, nsobj, namespaces, n; for (n = 0; n < 3; ++n) { if (r[n]) { namespaces = []; for (i = 0, len = r[n].length; i < len; ++i) { ns = r[n][i]; nsobj = { prefix: ns[0], delimiter: ns[1], extensions: undefined }; if (ns.length > 2) nsobj.extensions = {}; for (j = 2, len2 = ns.length; j < len2; j += 2) nsobj.extensions[ns[j]] = ns[j + 1]; namespaces.push(nsobj); } r[n] = namespaces; } } return { personal: r[0], other: r[1], shared: r[2] }; } function parseStatus(text, literals) { var r = parseExpr(text, literals), attrs = {}; // r[1] is [KEY1, VAL1, KEY2, VAL2, .... KEYn, VALn] for (var i = 0, len = r[1].length; i < len; i += 2) attrs[r[1][i].toLowerCase()] = r[1][i + 1]; return { name: utf7.decode(''+r[0]), attrs: attrs }; } function parseFetch(text, literals, seqno) { var list = parseExpr(text, literals)[0], attrs = {}, m, body; // list is [KEY1, VAL1, KEY2, VAL2, .... KEYn, VALn] for (var i = 0, len = list.length, key, val; i < len; i += 2) { key = list[i].toLowerCase(); val = list[i + 1]; if (key === 'envelope') val = parseFetchEnvelope(val); else if (key === 'internaldate') val = new Date(val); else if (key === 'modseq') // always a list of one value val = ''+val[0]; else if (key === 'body' || key === 'bodystructure') val = parseBodyStructure(val); else if (m = RE_BODYINLINEKEY.exec(list[i])) { // a body was sent as a non-literal val = new Buffer(''+val); body = new ReadableStream(); body._readableState.sync = false; body._read = EMPTY_READCB; this.emit('body', body, { seqno: seqno, which: m[1], size: val.length }); body.push(val); body.push(null); continue; } attrs[key] = val; } return attrs; } function parseBodyStructure(cur, literals, prefix, partID) { var ret = [], i, len; if (prefix === undefined) { var result = (Array.isArray(cur) ? cur : parseExpr(cur, literals)); if (result.length) ret = parseBodyStructure(result, literals, '', 1); } else { var part, partLen = cur.length, next; if (Array.isArray(cur[0])) { // multipart next = -1; while (Array.isArray(cur[++next])) { ret.push(parseBodyStructure(cur[next], literals, prefix + (prefix !== '' ? '.' : '') + (partID++).toString(), 1)); } part = { type: cur[next++].toLowerCase() }; if (partLen > next) { if (Array.isArray(cur[next])) { part.params = {}; for (i = 0, len = cur[next].length; i < len; i += 2) part.params[cur[next][i].toLowerCase()] = cur[next][i + 1]; } else part.params = cur[next]; ++next; } } else { // single part next = 7; if (typeof cur[1] === 'string') { part = { // the path identifier for this part, useful for fetching specific // parts of a message partID: (prefix !== '' ? prefix : '1'), // required fields as per RFC 3501 -- null or otherwise type: cur[0].toLowerCase(), subtype: cur[1].toLowerCase(), params: null, id: cur[3], description: cur[4], encoding: cur[5], size: cur[6] }; } else { // type information for malformed multipart body part = { type: cur[0] ? cur[0].toLowerCase() : null, params: null }; cur.splice(1, 0, null); ++partLen; next = 2; } if (Array.isArray(cur[2])) { part.params = {}; for (i = 0, len = cur[2].length; i < len; i += 2) part.params[cur[2][i].toLowerCase()] = cur[2][i + 1]; if (cur[1] === null) ++next; } if (part.type === 'message' && part.subtype === 'rfc822') { // envelope if (partLen > next && Array.isArray(cur[next])) part.envelope = parseFetchEnvelope(cur[next]); else part.envelope = null; ++next; // body if (partLen > next && Array.isArray(cur[next])) part.body = parseBodyStructure(cur[next], literals, prefix, 1); else part.body = null; ++next; } if ((part.type === 'text' || (part.type === 'message' && part.subtype === 'rfc822')) && partLen > next) part.lines = cur[next++]; if (typeof cur[1] === 'string' && partLen > next) part.md5 = cur[next++]; } // add any extra fields that may or may not be omitted entirely parseStructExtra(part, partLen, cur, next); ret.unshift(part); } return ret; } function parseStructExtra(part, partLen, cur, next) { if (partLen > next) { // disposition // null or a special k/v list with these kinds of values: // e.g.: ['Foo', null] // ['Foo', ['Bar', 'Baz']] // ['Foo', ['Bar', 'Baz', 'Bam', 'Pow']] var disposition = { type: null, params: null }; if (Array.isArray(cur[next])) { disposition.type = cur[next][0]; if (Array.isArray(cur[next][1])) { disposition.params = {}; for (var i = 0, len = cur[next][1].length, key; i < len; i += 2) { key = cur[next][1][i].toLowerCase(); disposition.params[key] = cur[next][1][i + 1]; } } } else if (cur[next] !== null) disposition.type = cur[next]; if (disposition.type === null) part.disposition = null; else part.disposition = disposition; ++next; } if (partLen > next) { // language can be a string or a list of one or more strings, so let's // make this more consistent ... if (cur[next] !== null) part.language = (Array.isArray(cur[next]) ? cur[next] : [cur[next]]); else part.language = null; ++next; } if (partLen > next) part.location = cur[next++]; if (partLen > next) { // extension stuff introduced by later RFCs // this can really be any value: a string, number, or (un)nested list // let's not parse it for now ... part.extensions = cur[next]; } } function parseFetchEnvelope(list) { return { date: new Date(list[0]), subject: decodeWords(list[1]), from: parseEnvelopeAddresses(list[2]), sender: parseEnvelopeAddresses(list[3]), replyTo: parseEnvelopeAddresses(list[4]), to: parseEnvelopeAddresses(list[5]), cc: parseEnvelopeAddresses(list[6]), bcc: parseEnvelopeAddresses(list[7]), inReplyTo: list[8], messageId: list[9] }; } function parseEnvelopeAddresses(list) { var addresses = null; if (Array.isArray(list)) { addresses = []; var inGroup = false, curGroup; for (var i = 0, len = list.length, addr; i < len; ++i) { addr = list[i]; if (addr[2] === null) { // end of group addresses inGroup = false; if (curGroup) { addresses.push(curGroup); curGroup = undefined; } } else if (addr[3] === null) { // start of group addresses inGroup = true; curGroup = { group: addr[2], addresses: [] }; } else { // regular user address var info = { name: decodeWords(addr[0]), mailbox: addr[2], host: addr[3] }; if (inGroup) curGroup.addresses.push(info); else if (!inGroup) addresses.push(info); } list[i] = addr; } if (inGroup) { // no end of group found, assume implicit end addresses.push(curGroup); } } return addresses; } function parseExpr(o, literals, result, start, useBrackets) { start = start || 0; var inQuote = false, lastPos = start - 1, isTop = false, isBody = false, escaping = false, val; if (useBrackets === undefined) useBrackets = true; if (!result) result = []; if (typeof o === 'string') { o = { str: o }; isTop = true; } for (var i = start, len = o.str.length; i < len; ++i) { if (!inQuote) { if (isBody) { if (o.str[i] === ']') { val = convStr(o.str.substring(lastPos + 1, i + 1), literals); result.push(val); lastPos = i; isBody = false; } } else if (o.str[i] === '"') inQuote = true; else if (o.str[i] === ' ' || o.str[i] === ')' || (useBrackets && o.str[i] === ']')) { if (i - (lastPos + 1) > 0) { val = convStr(o.str.substring(lastPos + 1, i), literals); result.push(val); } if ((o.str[i] === ')' || (useBrackets && o.str[i] === ']')) && !isTop) return i; lastPos = i; } else if ((o.str[i] === '(' || (useBrackets && o.str[i] === '['))) { if (o.str[i] === '[' && i - 4 >= start && o.str.substring(i - 4, i).toUpperCase() === 'BODY') { isBody = true; lastPos = i - 5; } else { var innerResult = []; i = parseExpr(o, literals, innerResult, i + 1, useBrackets); lastPos = i; result.push(innerResult); } } } else if (o.str[i] === '\\') escaping = !escaping; else if (o.str[i] === '"') { if (!escaping) inQuote = false; escaping = false; } if (i + 1 === len && len - (lastPos + 1) > 0) result.push(convStr(o.str.substring(lastPos + 1), literals)); } return (isTop ? result : start); } function convStr(str, literals) { if (str[0] === '"') { str = str.substring(1, str.length - 1); var newstr = '', isEscaping = false, p = 0; for (var i = 0, len = str.length; i < len; ++i) { if (str[i] === '\\') { if (!isEscaping) isEscaping = true; else { isEscaping = false; newstr += str.substring(p, i - 1); p = i; } } else if (str[i] === '"') { if (isEscaping) { isEscaping = false; newstr += str.substring(p, i - 1); p = i; } } } if (p === 0) return str; else { newstr += str.substring(p); return newstr; } } else if (str === 'NIL') return null; else if (RE_INTEGER.test(str)) { // some IMAP extensions utilize large (64-bit) integers, which JavaScript // can't handle natively, so we'll just keep it as a string if it's too big var val = parseInt(str, 10); return (val.toString() === str ? val : str); } else if (literals && literals.length && str === LITPLACEHOLDER) { var l = literals.shift(); if (Buffer.isBuffer(l)) l = l.toString('utf8'); return l; } return str; } function repeat(chr, len) { var s = ''; for (var i = 0; i < len; ++i) s += chr; return s; } function decodeBytes(buf, encoding, offset, mlen, pendoffset, state, nextBuf) { if (!jsencoding) jsencoding = require('../deps/encoding/encoding'); if (jsencoding.encodingExists(encoding)) { if (state.buffer !== undefined) { if (state.encoding === encoding && state.consecutive) { // concatenate buffer + current bytes in hopes of finally having // something that's decodable var newbuf = new Buffer(state.buffer.length + buf.length); state.buffer.copy(newbuf, 0); buf.copy(newbuf, state.buffer.length); buf = newbuf; } else { // either: // - the current encoded word is not separated by the previous partial // encoded word by linear whitespace, OR // - the current encoded word and the previous partial encoded word // use different encodings state.buffer = state.encoding = undefined; state.curReplace = undefined; } } var ret, isPartial = false; if (state.remainder !== undefined) { // use cached remainder from the previous lookahead ret = state.remainder; state.remainder = undefined; } else { try { ret = jsencoding.TextDecoder(encoding).decode(buf); } catch (e) { if (e.message.indexOf('Seeking') === 0) isPartial = true; } } if (!isPartial && nextBuf) { // try to decode a lookahead buffer (current buffer + next buffer) // and see if it starts with the decoded value of the current buffer. // if not, the current buffer is partial var lookahead, lookaheadBuf = new Buffer(buf.length + nextBuf.length); buf.copy(lookaheadBuf); nextBuf.copy(lookaheadBuf, buf.length); try { lookahead = jsencoding.TextDecoder(encoding).decode(lookaheadBuf); } catch(e) { // cannot decode the lookahead, do nothing } if (lookahead !== undefined) { if (lookahead.indexOf(ret) === 0) { // the current buffer is whole, cache the lookahead's remainder state.remainder = lookahead.substring(ret.length); } else { isPartial = true; ret = undefined; } } } if (ret !== undefined) { if (state.curReplace) { // we have some previous partials which were finally "satisfied" by the // current encoded word, so replace from the beginning of the first // partial to the end of the current encoded word state.replaces.push({ fromOffset: state.curReplace[0].fromOffset, toOffset: offset + mlen, val: ret }); state.replaces.splice(state.replaces.indexOf(state.curReplace), 1); state.curReplace = undefined; } else { // normal case where there are no previous partials and we successfully // decoded a single encoded word state.replaces.push({ // we ignore linear whitespace between consecutive encoded words fromOffset: state.consecutive ? pendoffset : offset, toOffset: offset + mlen, val: ret }); } state.buffer = state.encoding = undefined; return; } else if (isPartial) { // RFC2047 says that each decoded encoded word "MUST represent an integral // number of characters. A multi-octet character may not be split across // adjacent encoded-words." However, some MUAs appear to go against this, // so we join broken encoded words separated by linear white space until // we can successfully decode or we see a change in encoding state.encoding = encoding; state.buffer = buf; if (!state.curReplace) state.replaces.push(state.curReplace = []); state.curReplace.push({ fromOffset: offset, toOffset: offset + mlen, // the value we replace this encoded word with if it doesn't end up // becoming part of a successful decode val: repeat('\uFFFD', buf.length) }); return; } } // in case of unexpected error or unsupported encoding, just substitute the // raw bytes state.replaces.push({ fromOffset: offset, toOffset: offset + mlen, val: buf.toString('binary') }); } function qEncReplacer(match, byte) { if (match === '_') return ' '; else return String.fromCharCode(parseInt(byte, 16)); } function decodeWords(str, state) { var pendoffset = -1; if (!state) { state = { buffer: undefined, encoding: undefined, consecutive: false, replaces: undefined, curReplace: undefined, remainder: undefined }; } state.replaces = []; var bytes, m, next, i, j, leni, lenj, seq, replaces = [], lastReplace = {}; // join consecutive q-encoded words that have the same charset first while (m = RE_ENCWORD.exec(str)) { seq = { consecutive: (pendoffset > -1 ? RE_LWS_ONLY.test(str.substring(pendoffset, m.index)) : false), charset: m[1].toLowerCase(), encoding: m[2].toLowerCase(), chunk: m[3], index: m.index, length: m[0].length, pendoffset: pendoffset, buf: undefined }; lastReplace = replaces.length && replaces[replaces.length - 1]; if (seq.consecutive && seq.charset === lastReplace.charset && seq.encoding === lastReplace.encoding && seq.encoding === 'q') { lastReplace.length += seq.length + seq.index - pendoffset; lastReplace.chunk += seq.chunk; } else { replaces.push(seq); lastReplace = seq; } pendoffset = m.index + m[0].length; } // generate replacement substrings and their positions for (i = 0, leni = replaces.length; i < leni; ++i) { m = replaces[i]; state.consecutive = m.consecutive; if (m.encoding === 'q') { // q-encoding, similar to quoted-printable bytes = new Buffer(m.chunk.replace(RE_QENC, qEncReplacer), 'binary'); next = undefined; } else { // base64 bytes = m.buf || new Buffer(m.chunk, 'base64'); next = replaces[i + 1]; if (next && next.consecutive && next.encoding === m.encoding && next.charset === m.charset) { // we use the next base64 chunk, if any, to determine the integrity // of the current chunk next.buf = new Buffer(next.chunk, 'base64'); } } decodeBytes(bytes, m.charset, m.index, m.length, m.pendoffset, state, next && next.buf); } // perform the actual replacements for (i = state.replaces.length - 1; i >= 0; --i) { seq = state.replaces[i]; if (Array.isArray(seq)) { for (j = 0, lenj = seq.length; j < lenj; ++j) { str = str.substring(0, seq[j].fromOffset) + seq[j].val + str.substring(seq[j].toOffset); } } else { str = str.substring(0, seq.fromOffset) + seq.val + str.substring(seq.toOffset); } } return str; } function parseHeader(str, noDecode) { var lines = str.split(RE_CRLF), len = lines.length, header = {}, state = { buffer: undefined, encoding: undefined, consecutive: false, replaces: undefined, curReplace: undefined, remainder: undefined }, m, h, i, val; for (i = 0; i < len; ++i) { if (lines[i].length === 0) break; // empty line separates message's header and body if (lines[i][0] === '\t' || lines[i][0] === ' ') { if (!Array.isArray(header[h])) continue; // ignore invalid first line // folded header content val = lines[i]; if (!noDecode) { if (RE_ENCWORD_END.test(lines[i - 1]) && RE_ENCWORD_BEGIN.test(val)) { // RFC2047 says to *ignore* leading whitespace in folded header values // for adjacent encoded-words ... val = val.substring(1); } } header[h][header[h].length - 1] += val; } else { m = RE_HDR.exec(lines[i]); if (m) { h = m[1].toLowerCase().trim(); if (m[2]) { if (header[h] === undefined) header[h] = [m[2]]; else header[h].push(m[2]); } else header[h] = ['']; } else break; } } if (!noDecode) { var hvs; for (h in header) { hvs = header[h]; for (i = 0, len = header[h].length; i < len; ++i) hvs[i] = decodeWords(hvs[i], state); } } return header; } exports.Parser = Parser; exports.parseExpr = parseExpr; exports.parseEnvelopeAddresses = parseEnvelopeAddresses; exports.parseBodyStructure = parseBodyStructure; exports.parseHeader = parseHeader;