-
-
Save mscdex/39f6c71aa705bd5190a2 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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|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; | |
this.debug && this.debug('_parse(), top, literallen=' + this._literallen + ',body=' + (this._body ? true : false)); | |
this.debug && this.debug('_parse(), top, buffer: ' + inspect(this._buffer)); | |
this.debug && this.debug('_parse(), top, data(' + data.length + '): ' + inspect(data.toString('binary'))); | |
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.debug && this.debug('_parse(), before body.push() case #1'); | |
body.push(data.slice(0, litlen)); | |
this.debug && this.debug('_parse(), after body.push() case #1'); | |
} else { | |
this._literallen -= datalen; | |
this.debug && this.debug('_parse(), before body.push() case #2'); | |
var r = body.push(data); | |
this.debug && this.debug('_parse(), after body.push() case #2'); | |
if (!r && this._literallen > 0) { | |
this.debug && this.debug('_parse(), body.push() case #2 returned false'); | |
body._read = this._cbReadable; | |
return; | |
} | |
i = datalen; | |
} | |
this.debug && this.debug('_parse(), before literallen empty check'); | |
if (this._literallen === 0) { | |
this.debug && this.debug('_parse(), literallen === 0'); | |
this._body = undefined; | |
body._read = EMPTY_READCB; | |
body.push(null); | |
} | |
} else { | |
if (datalen > this._literallen) | |
this._literals.push(data.slice(0, this._literallen)); | |
else | |
this._literals.push(data); | |
i = this._literallen; | |
this._literallen = 0; | |
} | |
} | |
this.debug && this.debug('_parse(), before while, literallen=' + this._literallen + ',body=' + (this._body ? true : false) + ',i=' + i); | |
while (i < datalen) { | |
this.debug && this.debug('_parse(), top of while, literallen=' + this._literallen + ',body=' + (this._body ? true : false) + ',i=' + i); | |
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(); | |
this.debug && this.debug('_parse(), inside while, literallen=' + this._literallen + ',body=' + (this._body ? true : false) + ',i=' + i); | |
if (this._literallen > 0 && i < datalen) { | |
this.debug && this.debug('_parse(), inside while, unshifting: ' + inspect(data.slice(i).toString('binary'))); | |
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) { | |
this.debug && this.debug('_parse(), inside while, tryread() for non-body literal'); | |
// check if unshifted contents satisfies non-body literal length | |
this._tryread(this._literallen); | |
} | |
} | |
} else { | |
this.emit('other', this._buffer); | |
this._buffer = ''; | |
} | |
} | |
} | |
this.debug && this.debug('_parse(), after while, literallen=' + this._literallen + ',body=' + (this._body ? true : false) + ',i=' + i); | |
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') | |
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].toLowerCase(), 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] === ' ') { | |
// 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; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment