|
|
|
var utils = require('./imap.utilities');
|
|
|
|
|
|
|
|
var reCRLF = /\r\n/g,
|
|
|
|
reHdr = /^([^:]+):\s(.+)?$/,
|
|
|
|
reHdrFold = /^\s+(.+)$/;
|
|
|
|
|
|
|
|
exports.convStr = function(str, literals) {
|
|
|
|
if (str[0] === '"')
|
|
|
|
return str.substring(1, str.length-1);
|
|
|
|
else if (str === 'NIL')
|
|
|
|
return null;
|
|
|
|
else if (/^\d+$/.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.lp < literals.length && /^\{\d+\}$/.test(str))
|
|
|
|
return literals[literals.lp++];
|
|
|
|
else
|
|
|
|
return str;
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.parseHeaders = function(str) {
|
|
|
|
var lines = str.split(reCRLF),
|
|
|
|
headers = {}, m;
|
|
|
|
|
|
|
|
for (var i = 0, h, len = lines.length; i < len; ++i) {
|
|
|
|
if (lines[i].length === 0)
|
|
|
|
continue;
|
|
|
|
if (lines[i][0] === '\t' || lines[i][0] === ' ') {
|
|
|
|
// folded header content
|
|
|
|
m = reHdrFold.exec(lines[i]);
|
|
|
|
headers[h][headers[h].length - 1] += m[1];
|
|
|
|
} else {
|
|
|
|
m = reHdr.exec(lines[i]);
|
|
|
|
h = m[1].toLowerCase();
|
|
|
|
if (m[2]) {
|
|
|
|
if (headers[h] === undefined)
|
|
|
|
headers[h] = [m[2]];
|
|
|
|
else
|
|
|
|
headers[h].push(m[2]);
|
|
|
|
} else
|
|
|
|
headers[h] = [''];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return headers;
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.parseNamespaces = function(str, literals) {
|
|
|
|
var result, vals;
|
|
|
|
if (str.length === 3 && str.toUpperCase() === 'NIL')
|
|
|
|
vals = null;
|
|
|
|
else {
|
|
|
|
result = exports.parseExpr(str, literals);
|
|
|
|
vals = [];
|
|
|
|
for (var i = 0, len = result.length; i < len; ++i) {
|
|
|
|
var val = {
|
|
|
|
prefix: result[i][0],
|
|
|
|
delimiter: result[i][1]
|
|
|
|
};
|
|
|
|
if (result[i].length > 2) {
|
|
|
|
// extension data
|
|
|
|
val.extensions = [];
|
|
|
|
for (var j = 2, len2 = result[i].length; j < len2; j += 2) {
|
|
|
|
val.extensions.push({
|
|
|
|
name: result[i][j],
|
|
|
|
flags: result[i][j + 1]
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
vals.push(val);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return vals;
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.parseFetchBodies = function(str, literals) {
|
|
|
|
literals.lp = 0;
|
|
|
|
var result = exports.parseExpr(str, literals),
|
|
|
|
bodies;
|
|
|
|
for (var i = 0, len = result.length; i < len; i += 2) {
|
|
|
|
if (Array.isArray(result[i])) {
|
|
|
|
if (result[i].length === 0)
|
|
|
|
result[i].push('');
|
|
|
|
/*else if (result[i].length === 2 && typeof result[i][1] === 'number') {
|
|
|
|
// OK, so ordinarily this is where we'd transform a partial fetch
|
|
|
|
// key so that it matches up with a key in our request's fetchers object
|
|
|
|
// but since IMAP sucks, we have no way to match up a partial fetch
|
|
|
|
// response if less than the number of octets we requested were returned.
|
|
|
|
//
|
|
|
|
// Example: We request BODY[TEXT]<0.32>, but BODY[TEXT] is only 16 bytes,
|
|
|
|
// then we get back: BODY[TEXT]<0> {16}
|
|
|
|
// This leaves us with no way to find out what the original
|
|
|
|
// length request was. ARGH!!!^%&#^@#$%&$!
|
|
|
|
// Because of this and the fact that the server can return requested
|
|
|
|
// values in any order, I am disabling partial fetches entirely.
|
|
|
|
|
|
|
|
// BODY[TEXT]<0.32>
|
|
|
|
result[i][0] += '<';
|
|
|
|
result[i][0] += result[i][1]; // starting octet
|
|
|
|
result[i][0] += '.';
|
|
|
|
if (typeof result[i + 1] === 'number')
|
|
|
|
result[i][0] += result[i + 1];
|
|
|
|
else if (typeof
|
|
|
|
result[i][0] += '>';
|
|
|
|
}*/ else if (result[i].length > 1) {
|
|
|
|
// HEADER.FIELDS (foo) or HEADER.FIELDS (foo bar baz)
|
|
|
|
result[i][0] += ' (';
|
|
|
|
if (Array.isArray(result[i][1]))
|
|
|
|
result[i][0] += result[i][1].join(' ');
|
|
|
|
else
|
|
|
|
result[i][0] += result[i].slice(1).join(' ');
|
|
|
|
result[i][0] += ')';
|
|
|
|
}
|
|
|
|
if (bodies === undefined)
|
|
|
|
bodies = ['BODY[' + result[i][0] + ']', result[i + 1]];
|
|
|
|
else {
|
|
|
|
bodies.push('BODY[' + result[i][0] + ']');
|
|
|
|
bodies.push(result[i + 1]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return bodies;
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.parseFetch = function(str, literals, fetchData) {
|
|
|
|
literals.lp = 0;
|
|
|
|
var result = exports.parseExpr(str, literals);
|
|
|
|
for (var i = 0, len = result.length; i < len; i += 2) {
|
|
|
|
if (Array.isArray(result[i]))
|
|
|
|
continue;
|
|
|
|
result[i] = result[i].toUpperCase();
|
|
|
|
if (result[i] === 'UID')
|
|
|
|
fetchData.uid = parseInt(result[i + 1], 10);
|
|
|
|
else if (result[i] === 'INTERNALDATE')
|
|
|
|
fetchData.date = result[i + 1];
|
|
|
|
else if (result[i] === 'FLAGS')
|
|
|
|
fetchData.flags = result[i + 1].filter(utils.isNotEmpty);
|
|
|
|
else if (result[i] === 'BODYSTRUCTURE')
|
|
|
|
fetchData.structure = exports.parseBodyStructure(result[i + 1], literals);
|
|
|
|
else if (result[i] === 'RFC822.SIZE')
|
|
|
|
fetchData.size = parseInt(result[i + 1], 10);
|
|
|
|
else if (typeof result[i] === 'string') // simple extensions
|
|
|
|
fetchData[result[i].toLowerCase()] = result[i + 1];
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.parseBodyStructure = function(cur, literals, prefix, partID) {
|
|
|
|
var ret = [], i, len;
|
|
|
|
if (prefix === undefined) {
|
|
|
|
var result = (Array.isArray(cur) ? cur : exports.parseExpr(cur, literals));
|
|
|
|
if (result.length)
|
|
|
|
ret = exports.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(exports.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 = {};
|
|
|
|
for (i = 0, len = cur[next].length; i < len; ++i) {
|
|
|
|
if (i === 0)
|
|
|
|
part.envelope.date = cur[next][i];
|
|
|
|
else if (i === 1)
|
|
|
|
part.envelope.subject = cur[next][i];
|
|
|
|
else if (i >= 2 && i <= 7) {
|
|
|
|
var val = cur[next][i];
|
|
|
|
if (Array.isArray(val)) {
|
|
|
|
var addresses = [], inGroup = false, curGroup;
|
|
|
|
for (var j = 0, len2 = val.length; j < len2; ++j) {
|
|
|
|
if (val[j][3] === null) { // start group addresses
|
|
|
|
inGroup = true;
|
|
|
|
curGroup = {
|
|
|
|
group: val[j][2],
|
|
|
|
addresses: []
|
|
|
|
};
|
|
|
|
} else if (val[j][2] === null) { // end of group addresses
|
|
|
|
inGroup = false;
|
|
|
|
addresses.push(curGroup);
|
|
|
|
} else { // regular user address
|
|
|
|
var info = {
|
|
|
|
name: val[j][0],
|
|
|
|
mailbox: val[j][2],
|
|
|
|
host: val[j][3]
|
|
|
|
};
|
|
|
|
if (inGroup)
|
|
|
|
curGroup.addresses.push(info);
|
|
|
|
else
|
|
|
|
addresses.push(info);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
val = addresses;
|
|
|
|
}
|
|
|
|
if (i === 2)
|
|
|
|
part.envelope.from = val;
|
|
|
|
else if (i === 3)
|
|
|
|
part.envelope.sender = val;
|
|
|
|
else if (i === 4)
|
|
|
|
part.envelope['reply-to'] = val;
|
|
|
|
else if (i === 5)
|
|
|
|
part.envelope.to = val;
|
|
|
|
else if (i === 6)
|
|
|
|
part.envelope.cc = val;
|
|
|
|
else if (i === 7)
|
|
|
|
part.envelope.bcc = val;
|
|
|
|
} else if (i === 8)
|
|
|
|
// message ID being replied to
|
|
|
|
part.envelope['in-reply-to'] = cur[next][i];
|
|
|
|
else if (i === 9)
|
|
|
|
part.envelope['message-id'] = cur[next][i];
|
|
|
|
else
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} else
|
|
|
|
part.envelope = null;
|
|
|
|
++next;
|
|
|
|
|
|
|
|
// body
|
|
|
|
if (partLen > next && Array.isArray(cur[next]))
|
|
|
|
part.body = exports.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
|
|
|
|
exports.parseStructExtra(part, partLen, cur, next);
|
|
|
|
ret.unshift(part);
|
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.parseStructExtra = function(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];
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.parseExpr = function(o, literals, result, start) {
|
|
|
|
start = start || 0;
|
|
|
|
var inQuote = false, lastPos = start - 1, isTop = false, inLitStart = false,
|
|
|
|
val;
|
|
|
|
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 (o.str[i] === '"')
|
|
|
|
inQuote = true;
|
|
|
|
else if (o.str[i] === ' ' || o.str[i] === ')' || o.str[i] === ']') {
|
|
|
|
if (i - (lastPos + 1) > 0) {
|
|
|
|
val = exports.convStr(o.str.substring(lastPos + 1, i), literals);
|
|
|
|
result.push(val);
|
|
|
|
}
|
|
|
|
if ((o.str[i] === ')' || o.str[i] === ']') && !isTop)
|
|
|
|
return i;
|
|
|
|
lastPos = i;
|
|
|
|
} else if ((o.str[i] === '(' || o.str[i] === '[')) {
|
|
|
|
var innerResult = [];
|
|
|
|
i = exports.parseExpr(o, literals, innerResult, i + 1);
|
|
|
|
lastPos = i;
|
|
|
|
result.push(innerResult);
|
|
|
|
}/* else if (i > 0 && o.str[i] === '<' && o.str[i - 1] === ']') {
|
|
|
|
lastPos = i;
|
|
|
|
inLitStart = true;
|
|
|
|
} else if (o.str[i] === '>' && inLitStart) {
|
|
|
|
val = exports.convStr(o.str.substring(lastPos + 1, i), literals);
|
|
|
|
result[result.length - 1].push(val);
|
|
|
|
inLitStart = false;
|
|
|
|
}*/
|
|
|
|
} else if (o.str[i] === '"' &&
|
|
|
|
(o.str[i - 1] &&
|
|
|
|
(o.str[i - 1] !== '\\'
|
|
|
|
|| (o.str[i - 2] && o.str[i - 2] === '\\')
|
|
|
|
)))
|
|
|
|
inQuote = false;
|
|
|
|
if (i + 1 === len && len - (lastPos + 1) > 0)
|
|
|
|
result.push(exports.convStr(o.str.substring(lastPos + 1), literals));
|
|
|
|
}
|
|
|
|
return (isTop ? result : start);
|
|
|
|
};
|